tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from datetime import datetime, timedelta
  41from dateutil.tz import tzlocal, tzutc
  42from time import sleep
  43
  44import re
  45import json
  46import requests
  47import traceback as tb
  48from typing import Union
  49
  50from multiprocessing import cpu_count
  51from multiprocessing.pool import ThreadPool
  52import pandas as pd
  53
  54from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  55
  56from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator
  57from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  58
  59import UniLogger as uLog  # Logger for TKSBrokerAPI
  60
  61
  62# --- Common technical parameters:
  63
  64PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  65uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  66uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  67uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  68
  69__version__ = "1.5"  # The "major.minor" version setup here, but build number define at the build-server only
  70
  71CPU_COUNT = cpu_count()  # host's real CPU count
  72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  73
  74# --- Main constants:
  75
  76NANO = 0.000000001  # SI-constant nano = 10^-9
  77
  78
  79def NanoToFloat(units: str, nano: int) -> float:
  80    """
  81    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
  82
  83    `NanoToFloat(units="2", nano=500000000) -> 2.5`
  84
  85    `NanoToFloat(units="0", nano=50000000) -> 0.05`
  86
  87    :param units: integer string or integer parameter that represents the integer part of number
  88    :param nano: integer string or integer parameter that represents the fractional part of number
  89    :return: float view of number
  90    """
  91    return int(units) + int(nano) * NANO
  92
  93
  94def FloatToNano(number: float) -> dict:
  95    """
  96    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
  97
  98    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
  99
 100    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
 101
 102    :param number: float number
 103    :return: nano-type view of number: `{"units": "string", "nano": integer}`
 104    """
 105    splitByPoint = str(number).split(".")
 106    frac = 0
 107
 108    if len(splitByPoint) > 1:
 109        if len(splitByPoint[1]) <= 9:
 110            frac = int("{}{}".format(
 111                int(splitByPoint[1]),
 112                "0" * (9 - len(splitByPoint[1])),
 113            ))
 114
 115    if (number < 0) and (frac > 0):
 116        frac = -frac
 117
 118    return {"units": str(int(number)), "nano": frac}
 119
 120
 121def GetDatesAsString(start: str = None, end: str = None) -> tuple:
 122    """
 123    Create tuple of date and time strings with timezone parsed from user-friendly date.
 124
 125    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
 126
 127    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
 128    An error exception will occur if input date has incorrect format.
 129
 130    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
 131    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
 132    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
 133    Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago.
 134
 135    Also, you can use keywords for start if `end=None`:
 136    `today` (from 00:00:00 to the end of current day),
 137    `yesterday` (-1 day from 00:00:00 to 23:59:59),
 138    `week` (-7 day from 00:00:00 to the end of current day),
 139    `month` (-30 day from 00:00:00 to the end of current day),
 140    `year` (-365 day from 00:00:00 to the end of current day),
 141
 142    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
 143             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
 144             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
 145    """
 146    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
 147    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
 148    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
 149
 150    # time between start and the end of the current day:
 151    if start is None or start.lower() == "today":
 152        pass
 153
 154    # from start of the last day to the end of the last day:
 155    elif start.lower() == "yesterday":
 156        s -= timedelta(days=1)
 157        e -= timedelta(days=1)
 158
 159    # week (-7 day from 00:00:00 to the end of the current day):
 160    elif start.lower() == "week":
 161        s -= timedelta(days=6)  # +1 current day already taken into account
 162
 163    # month (-30 day from 00:00:00 to the end of current day):
 164    elif start.lower() == "month":
 165        s -= timedelta(days=29)  # +1 current day already taken into account
 166
 167    # year (-365 day from 00:00:00 to the end of current day):
 168    elif start.lower() == "year":
 169        s -= timedelta(days=364)  # +1 current day already taken into account
 170
 171    # -N days ago to the end of current day:
 172    elif start.startswith('-') and start[1:].isdigit():
 173        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
 174
 175    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
 176    else:
 177        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
 178        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
 179
 180    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
 181    s = s.strftime(TKS_DATE_TIME_FORMAT)
 182    e = e.strftime(TKS_DATE_TIME_FORMAT)
 183
 184    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
 185
 186    return s, e
 187
 188
 189class TinkoffBrokerServer:
 190    """
 191    This class implements methods to work with Tinkoff broker server.
 192
 193    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 194
 195    About `token`: https://tinkoff.github.io/investAPI/token/
 196    """
 197    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 198        """
 199        Main class init.
 200
 201        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 202        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 203                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 204        :param useCache: use default cache file with raw data to use instead of `iList`.
 205                         True by default. Cache is auto-update if new day has come.
 206                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 207        :param defaultCache: path to default cache file. `dump.json` by default.
 208        """
 209        if token is None or not token:
 210            try:
 211                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 212                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 213
 214            except KeyError:
 215                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 216                raise Exception("Token required")
 217
 218        else:
 219            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 220            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 221
 222        if accountId is None or not accountId:
 223            try:
 224                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 225                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 226
 227            except KeyError:
 228                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 229
 230        else:
 231            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 232            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 233
 234        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 235        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 236
 237        Latest version: https://pypi.org/project/tksbrokerapi/
 238        """
 239
 240        self.aliases = TKS_TICKER_ALIASES
 241        """Some aliases instead official tickers.
 242
 243        See also: `TKSEnums.TKS_TICKER_ALIASES`
 244        """
 245
 246        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 247
 248        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 249
 250        self.ticker = ""
 251        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 252
 253        See also: `SearchByTicker()`, `SearchInstruments()`.
 254        """
 255
 256        self.figi = ""
 257        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 258
 259        See also: `SearchByFIGI()`, `SearchInstruments()`.
 260        """
 261
 262        self.depth = 1
 263        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 264
 265        See also: `GetCurrentPrices()`.
 266        """
 267
 268        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 269        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 270
 271        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 272        """
 273
 274        uLogger.debug("Broker API server: {}".format(self.server))
 275
 276        self.timeout = 15
 277        """Server operations timeout in seconds. Default: `15`.
 278
 279        See also: `SendAPIRequest()`.
 280        """
 281
 282        self.headers = {
 283            "Content-Type": "application/json",
 284            "accept": "application/json",
 285            "Authorization": "Bearer {}".format(self.token),
 286            "x-app-name": "Tim55667757.TKSBrokerAPI",
 287        }
 288        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 289
 290        See also: `SendAPIRequest()`.
 291        """
 292
 293        self.body = None
 294        """Request body which send to broker server. Default: `None`.
 295
 296        See also: `SendAPIRequest()`.
 297        """
 298
 299        self.historyFile = None
 300        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 301
 302        See also: `History()`.
 303        """
 304
 305        self.htmlHistoryFile = "index.html"
 306        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 307
 308        See also: `ShowHistoryChart()`.
 309        """
 310
 311        self.instrumentsFile = "instruments.md"
 312        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 313
 314        See also: `ShowInstrumentsInfo()`.
 315        """
 316
 317        self.searchResultsFile = "search-results.md"
 318        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 319
 320        See also: `SearchInstruments()`.
 321        """
 322
 323        self.pricesFile = "prices.md"
 324        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 325
 326        See also: `GetListOfPrices()`.
 327        """
 328
 329        self.infoFile = "info.md"
 330        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 331
 332        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 333        """
 334
 335        self.bondsXLSXFile = "ext-bonds.xlsx"
 336        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 337        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 338
 339        See also: `ExtendBondsData()`.
 340        """
 341
 342        self.calendarFile = "calendar.md"
 343        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 344        
 345        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 346
 347        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 348        """
 349
 350        self.overviewFile = "overview.md"
 351        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 352
 353        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 354        """
 355
 356        self.overviewDigestFile = "overview-digest.md"
 357        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 358
 359        See also: `Overview()` with parameter `details="digest"`.
 360        """
 361
 362        self.overviewPositionsFile = "overview-positions.md"
 363        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 364
 365        See also: `Overview()` with parameter `details="positions"`.
 366        """
 367
 368        self.overviewOrdersFile = "overview-orders.md"
 369        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 370
 371        See also: `Overview()` with parameter `details="orders"`.
 372        """
 373
 374        self.overviewAnalyticsFile = "overview-analytics.md"
 375        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 376
 377        See also: `Overview()` with parameter `details="analytics"`.
 378        """
 379
 380        self.reportFile = "deals.md"
 381        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 382
 383        See also: `Deals()`.
 384        """
 385
 386        self.withdrawalLimitsFile = "limits.md"
 387        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 388
 389        See also: `OverviewLimits()` and `RequestLimits()`.
 390        """
 391
 392        self.userInfoFile = "user-info.md"
 393        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 394
 395        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 396        """
 397
 398        self.userAccountsFile = "accounts.md"
 399        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 400
 401        See also: `OverviewAccounts()`, `RequestAccounts()`.
 402        """
 403
 404        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 405        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 406
 407        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 408
 409        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 410        """
 411
 412        self.iList = None  # init iList for raw instruments data
 413        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 414        
 415        See also: `Listing()`, `DumpInstruments()`.
 416        """
 417
 418        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 419        if useCache:
 420            if os.path.exists(self.iListDumpFile):
 421                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 422                curTime = datetime.now(tzutc())
 423
 424                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 425                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 426
 427                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 428
 429                else:
 430                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 431
 432                    uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile)))
 433                    uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 434
 435            else:
 436                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 437                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 438
 439        else:
 440            self.iList = self.Listing()  # request new raw instruments data from broker server
 441            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 442
 443        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 444        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 445
 446        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 447        """
 448
 449    @staticmethod
 450    def _ParseJSON(rawData="{}", debug: bool = False) -> dict:
 451        """
 452        Parse JSON from response string.
 453
 454        :param rawData: this is a string with JSON-formatted text.
 455        :param debug: if `True` then print more debug information.
 456        :return: JSON (dictionary), parsed from server response string.
 457        """
 458        if debug:
 459            uLogger.debug("Raw text body:")
 460            uLogger.debug(rawData)
 461
 462        responseJSON = json.loads(rawData) if rawData else {}
 463
 464        if debug:
 465            uLogger.debug("JSON formatted:")
 466            for jsonLine in json.dumps(responseJSON, indent=4).split('\n'):
 467                uLogger.debug(jsonLine)
 468
 469        return responseJSON
 470
 471    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
 472        """
 473        Send GET or POST request to broker server and receive JSON object.
 474
 475        self.header: must be defining with dictionary of headers.
 476        self.body: if define then used as request body. None by default.
 477        self.timeout: global request timeout, 15 seconds by default.
 478        :param url: url with REST request.
 479        :param reqType: send "GET" or "POST" request. "GET" by default.
 480        :param retry: how many times retry after first request if an 5xx server errors occurred.
 481        :param pause: sleep time in seconds between retries.
 482        :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc.
 483        :return: response JSON (dictionary) from broker.
 484        """
 485        if reqType not in ("GET", "POST"):
 486            uLogger.error("You can define request type: 'GET' or 'POST'!")
 487            raise Exception("Incorrect value")
 488
 489        if debug:
 490            uLogger.debug("Request parameters:")
 491            uLogger.debug("    - REST API URL: {}".format(url))
 492            uLogger.debug("    - request type: {}".format(reqType))
 493            uLogger.debug("    - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***")))
 494            uLogger.debug("    - body: {}".format(self.body))
 495
 496        # fast hack to avoid all operations with some tickers/FIGI
 497        responseJSON = {}
 498        oK = True
 499        for item in self.exclude:
 500            if item in url:
 501                if debug:
 502                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 503
 504                oK = False
 505                break
 506
 507        if oK:
 508            counter = 0
 509            response = None
 510            errMsg = ""
 511
 512            while not response and counter <= retry:
 513                if reqType == "GET":
 514                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 515
 516                if reqType == "POST":
 517                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 518
 519                if debug:
 520                    uLogger.debug("Response:")
 521                    uLogger.debug("    - status code: {}".format(response.status_code))
 522                    uLogger.debug("    - reason: {}".format(response.reason))
 523                    uLogger.debug("    - body length: {}".format(len(response.text)))
 524                    uLogger.debug("    - headers: {}".format(response.headers))
 525
 526                # Server returns some headers:
 527                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
 528                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
 529                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
 530                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 531                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 532                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 533                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 534                    sleep(rateLimitWait)
 535
 536                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 537                if 400 <= response.status_code < 500:
 538                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 539                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 540                    counter = retry + 1
 541
 542                if 500 <= response.status_code < 600:
 543                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 544                    uLogger.debug("    - not oK, {}".format(errMsg))
 545                    counter += 1
 546
 547                    if counter <= retry:
 548                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 549                        sleep(pause)
 550
 551            responseJSON = self._ParseJSON(response.text)
 552
 553            if errMsg:
 554                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 555                uLogger.error("    - not oK, {}".format(errMsg))
 556
 557        return responseJSON
 558
 559    def _IUpdater(self, iType: str) -> tuple:
 560        """
 561        Request instrument by type from server. See available API methods for instruments:
 562        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 563        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 564        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 565        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 566        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 567
 568        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 569        :return: tuple with iType name and list of available instruments of current type for defined user token.
 570        """
 571        result = []
 572
 573        if iType in TKS_INSTRUMENTS:
 574            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 575
 576            # all instruments have the same body in API v2 requests:
 577            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 578            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 579            result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"]
 580
 581        return iType, result
 582
 583    def _IWrapper(self, kwargs):
 584        """
 585        Wrapper runs instrument's update method `_IUpdater()`.
 586        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 587        """
 588        return self._IUpdater(**kwargs)
 589
 590    def Listing(self) -> dict:
 591        """
 592        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 593
 594        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 595        """
 596        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 597        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 598
 599        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 600        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 601        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 602
 603        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 604        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 605        poolUpdater.close()
 606
 607        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 608        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 609        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 610
 611        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 612        for iType in iList.keys():
 613            for ticker in iList[iType]:
 614                iList[iType][ticker]["type"] = iType
 615
 616                if "minPriceIncrement" in iList[iType][ticker].keys():
 617                    iList[iType][ticker]["step"] = NanoToFloat(
 618                        iList[iType][ticker]["minPriceIncrement"]["units"],
 619                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 620                    )
 621
 622                else:
 623                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 624
 625        return iList
 626
 627    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 628        """
 629        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 630
 631        See also: `DumpInstruments()`, `Listing()`.
 632
 633        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 634                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 635        """
 636        if self.iListDumpFile is None or not self.iListDumpFile:
 637            uLogger.error("Output name of dump file must be defined!")
 638            raise Exception("Filename required")
 639
 640        if not self.iList or forceUpdate:
 641            self.iList = self.Listing()
 642
 643        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 644
 645        # Save as XLSX with separated sheets for every type of instruments:
 646        with pd.ExcelWriter(
 647                path=xlsxDumpFile,
 648                date_format=TKS_DATE_FORMAT,
 649                datetime_format=TKS_DATE_TIME_FORMAT,
 650                mode="w",
 651        ) as writer:
 652            for iType in TKS_INSTRUMENTS:
 653                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 654                df = df[sorted(df)]  # sorted by column names
 655                df = df.applymap(
 656                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 657                    na_action="ignore",
 658                )  # converting numbers from nano-type to float in every cell
 659                df.to_excel(
 660                    writer,
 661                    sheet_name=iType,
 662                    encoding="UTF-8",
 663                    freeze_panes=(1, 1),
 664                )  # saving as XLSX-file with freeze first row and column as headers
 665
 666        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 667
 668    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 669        """
 670        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 671        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 672
 673        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 674
 675        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 676                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 677        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 678        """
 679        if self.iListDumpFile is None or not self.iListDumpFile:
 680            uLogger.error("Output name of dump file must be defined!")
 681            raise Exception("Filename required")
 682
 683        if not self.iList or forceUpdate:
 684            self.iList = self.Listing()
 685
 686        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 687        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 688            fH.write(jsonDump)
 689
 690        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 691
 692        return jsonDump
 693
 694    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 695        """
 696        Show information about one instrument defined by json data and prints it in Markdown format.
 697
 698        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 699
 700        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 701        :param show: if `True` then also printing information about instrument and its current price.
 702        :return: multilines text in Markdown format with information about one instrument.
 703        """
 704        splitLine = "|                                                             |                                                        |\n"
 705        infoText = ""
 706
 707        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 708            info = [
 709                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 710                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 711                "| Parameters                                                  | Values                                                 |\n",
 712                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 713                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 714                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 715            ]
 716
 717            if "sector" in iJSON.keys() and iJSON["sector"]:
 718                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 719
 720            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 721                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 722                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 723            )))
 724
 725            info.extend([
 726                splitLine,
 727                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 728                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 729            ])
 730
 731            if "isin" in iJSON.keys() and iJSON["isin"]:
 732                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 733
 734            if "classCode" in iJSON.keys():
 735                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 736
 737            info.extend([
 738                splitLine,
 739                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 740                splitLine,
 741                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 742                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 743                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 744            ])
 745
 746            if iJSON["figi"]:
 747                self.figi = iJSON["figi"]
 748                iJSON = iJSON | self.RequestTradingStatus()
 749
 750                info.extend([
 751                    splitLine,
 752                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 753                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 754                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 755                ])
 756
 757            info.append(splitLine)
 758
 759            if "type" in iJSON.keys() and iJSON["type"]:
 760                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 761
 762            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 763                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 764
 765            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 766                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 767
 768            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 769                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 770
 771            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 772                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 773
 774            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 775                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 776
 777            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 778                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 779
 780            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 781                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 782
 783            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 784                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 785
 786            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 787                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 788
 789            if "currency" in iJSON.keys():
 790                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 791
 792            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 793                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 794
 795            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 796                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 797
 798            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 799                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 800
 801            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 802                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 803
 804            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 805                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 806
 807            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 808                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 809
 810            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 811                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 812
 813            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 814                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 815
 816            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 817                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 818
 819            iExt = None
 820            if iJSON["type"] == "Bonds":
 821                info.extend([
 822                    splitLine,
 823                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 824                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 825                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 826                        iJSON["nominal"]["currency"],
 827                    )),
 828                ])
 829
 830                if "floatingCouponFlag" in iJSON.keys():
 831                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 832
 833                if "amortizationFlag" in iJSON.keys():
 834                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 835
 836                info.append(splitLine)
 837
 838                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 839                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 840
 841                iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 842
 843                info.extend([
 844                    "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 845                    "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 846                    "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 847                ])
 848
 849                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 850                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 851                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 852                        iJSON["aciValue"]["currency"]
 853                    )))
 854
 855            if "currentPrice" in iJSON.keys():
 856                info.append(splitLine)
 857
 858                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 859                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 860
 861                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 862                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 863                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 864                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 865                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 866
 867                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 868                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 869
 870                info.extend([
 871                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 872                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 873                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 874                    )),
 875                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 876                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 877                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 878                    )),
 879                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 880                        "{:.2f}%{}".format(
 881                            iJSON["currentPrice"]["changes"],
 882                            " ({}{:.2f} {})".format(
 883                                "+" if bondChangesDelta > 0 else "",
 884                                bondChangesDelta,
 885                                aciCurrency
 886                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 887                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 888                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 889                                currency
 890                            ),
 891                        )
 892                    ),
 893                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 894                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 895                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 896                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 897                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 898                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 899                    )),
 900                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 901                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 902                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 903                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 904                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 905                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 906                    )),
 907                ])
 908
 909            if "lot" in iJSON.keys():
 910                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 911
 912            if "step" in iJSON.keys() and iJSON["step"] != 0:
 913                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
 914
 915            # Add bond payment calendar:
 916            if iJSON["type"] == "Bonds":
 917                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 918                info.extend(["\n", strCalendar])
 919
 920            infoText += "".join(info)
 921
 922            if show:
 923                uLogger.info("{}".format(infoText))
 924
 925            else:
 926                uLogger.debug("{}".format(infoText))
 927
 928            if self.infoFile is not None:
 929                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 930                    fH.write(infoText)
 931
 932                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 933
 934        return infoText
 935
 936    def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 937        """
 938        Search and return raw broker's information about instrument by its ticker.
 939        `ticker` must be defined! If debug=True then print all debug messages.
 940
 941        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 942        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 943        :param debug: if `True` then print all debug console messages.
 944        :return: JSON formatted data with information about instrument.
 945        """
 946        tickerJSON = {}
 947        if debug:
 948            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 949
 950        if not self.ticker:
 951            uLogger.warning("self.ticker variable is not be empty!")
 952
 953        else:
 954            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 955                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 956                raise Exception("Instrument not allowed")
 957
 958            if not self.iList:
 959                self.iList = self.Listing()
 960
 961            if self.ticker in self.iList["Shares"].keys():
 962                tickerJSON = self.iList["Shares"][self.ticker]
 963                if debug:
 964                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 965
 966            elif self.ticker in self.iList["Currencies"].keys():
 967                tickerJSON = self.iList["Currencies"][self.ticker]
 968                if debug:
 969                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 970
 971            elif self.ticker in self.iList["Bonds"].keys():
 972                tickerJSON = self.iList["Bonds"][self.ticker]
 973                if debug:
 974                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 975
 976            elif self.ticker in self.iList["Etfs"].keys():
 977                tickerJSON = self.iList["Etfs"][self.ticker]
 978                if debug:
 979                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 980
 981            elif self.ticker in self.iList["Futures"].keys():
 982                tickerJSON = self.iList["Futures"][self.ticker]
 983                if debug:
 984                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 985
 986        if tickerJSON:
 987            self.figi = tickerJSON["figi"]
 988
 989            if requestPrice:
 990                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 991
 992                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 993                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 994
 995                else:
 996                    tickerJSON["currentPrice"]["changes"] = 0
 997
 998            if show:
 999                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
1000
1001        else:
1002            if show:
1003                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1004
1005        return tickerJSON
1006
1007    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1008        """
1009        Search and return raw broker's information about instrument by its FIGI.
1010        `figi` must be defined! If debug=True then print all debug messages.
1011
1012        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1013        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1014        :param debug: if `True` then print all debug console messages.
1015        :return: JSON formatted data with information about instrument.
1016        """
1017        figiJSON = {}
1018        if debug:
1019            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1020
1021        if not self.figi:
1022            uLogger.warning("self.figi variable is not be empty!")
1023
1024        else:
1025            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1026                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1027                raise Exception("Instrument not allowed")
1028
1029            if not self.iList:
1030                self.iList = self.Listing()
1031
1032            for item in self.iList["Shares"].keys():
1033                if self.figi == self.iList["Shares"][item]["figi"]:
1034                    figiJSON = self.iList["Shares"][item]
1035
1036                    if debug:
1037                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1038
1039                    break
1040
1041            if not figiJSON:
1042                for item in self.iList["Currencies"].keys():
1043                    if self.figi == self.iList["Currencies"][item]["figi"]:
1044                        figiJSON = self.iList["Currencies"][item]
1045
1046                        if debug:
1047                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1048
1049                        break
1050
1051            if not figiJSON:
1052                for item in self.iList["Bonds"].keys():
1053                    if self.figi == self.iList["Bonds"][item]["figi"]:
1054                        figiJSON = self.iList["Bonds"][item]
1055
1056                        if debug:
1057                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1058
1059                        break
1060
1061            if not figiJSON:
1062                for item in self.iList["Etfs"].keys():
1063                    if self.figi == self.iList["Etfs"][item]["figi"]:
1064                        figiJSON = self.iList["Etfs"][item]
1065
1066                        if debug:
1067                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1068
1069                        break
1070
1071            if not figiJSON:
1072                for item in self.iList["Futures"].keys():
1073                    if self.figi == self.iList["Futures"][item]["figi"]:
1074                        figiJSON = self.iList["Futures"][item]
1075
1076                        if debug:
1077                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1078
1079                        break
1080
1081        if figiJSON:
1082            self.figi = figiJSON["figi"]
1083            self.ticker = figiJSON["ticker"]
1084
1085            if requestPrice:
1086                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1087
1088                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1089                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1090
1091                else:
1092                    figiJSON["currentPrice"]["changes"] = 0
1093
1094            if show:
1095                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1096
1097        else:
1098            if show:
1099                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1100
1101        return figiJSON
1102
1103    def GetCurrentPrices(self, show: bool = True) -> dict:
1104        """
1105        Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
1106        `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1107
1108        See also:
1109
1110        :param show: if `True` then print DOM to log and console.
1111        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1112        """
1113        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1114
1115        if self.depth < 1:
1116            uLogger.error("Depth of Market (DOM) must be >=1!")
1117            raise Exception("Incorrect value")
1118
1119        if not (self.ticker or self.figi):
1120            uLogger.error("self.ticker or self.figi variables must be defined!")
1121            raise Exception("Ticker or FIGI required")
1122
1123        if self.ticker and not self.figi:
1124            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1125            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1126
1127        if not self.ticker and self.figi:
1128            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1129            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1130
1131        if not self.figi:
1132            uLogger.error("FIGI is not defined!")
1133            raise Exception("Ticker or FIGI required")
1134
1135        else:
1136            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1137
1138            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1139            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1140            self.body = str({"figi": self.figi, "depth": self.depth})
1141            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")
1142
1143            if pricesResponse:
1144                # list of dicts with sellers orders:
1145                prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1146
1147                # list of dicts with buyers orders:
1148                prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1149
1150                # max price of instrument at this time:
1151                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1152
1153                # min price of instrument at this time:
1154                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1155
1156                # last price of deal with instrument:
1157                prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0
1158
1159                # last close price of instrument:
1160                prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0
1161
1162            else:
1163                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1164                uLogger.debug("Server response: {}".format(pricesResponse))
1165
1166            if show:
1167                if prices["buy"] or prices["sell"]:
1168                    info = [
1169                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1170                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1171                            self.ticker,
1172                            self.figi,
1173                            self.depth,
1174                        ),
1175                        uLog.sepShort, "\n",
1176                        " Orders of Buyers   | Orders of Sellers\n",
1177                        uLog.sepShort, "\n",
1178                        " Sell prices (vol.) | Buy prices (vol.)\n",
1179                        uLog.sepShort, "\n",
1180                    ]
1181
1182                    if not prices["buy"]:
1183                        info.append("                    | No orders!\n")
1184                        sumBuy = 0
1185
1186                    else:
1187                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1188                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1189                        for item in maxMinSorted:
1190                            info.append("                    | {} ({})\n".format(item["price"], item["quantity"]))
1191
1192                    if not prices["sell"]:
1193                        info.append("No orders!          |\n")
1194                        sumSell = 0
1195
1196                    else:
1197                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1198                        for item in prices["sell"]:
1199                            info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1200
1201                    info.extend([
1202                        uLog.sepShort, "\n",
1203                        "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1204                        uLog.sepShort, "\n",
1205                    ])
1206
1207                    infoText = "".join(info)
1208
1209                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1210
1211                else:
1212                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1213
1214        return prices
1215
1216    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1217        """
1218        This method get and show information about all available broker instruments for current user account.
1219        If `instrumentsFile` string is not empty then also save information to this file.
1220
1221        :param show: if `True` then print results to console, if `False` - print only to file.
1222        :return: multi-lines string with all available broker instruments
1223        """
1224        if not self.iList:
1225            self.iList = self.Listing()
1226
1227        info = [
1228            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1229            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1230        ]
1231
1232        # add instruments count by type:
1233        for iType in self.iList.keys():
1234            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1235
1236        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1237        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1238
1239        # generating info tables with all instruments by type:
1240        for iType in self.iList.keys():
1241            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1242
1243            for instrument in self.iList[iType].keys():
1244                iName = self.iList[iType][instrument]["name"]  # instrument's name
1245                if len(iName) > 57:
1246                    iName = "{}...".format(iName[:54])  # right trim for a long string
1247
1248                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1249                    self.iList[iType][instrument]["ticker"],
1250                    iName,
1251                    self.iList[iType][instrument]["figi"],
1252                    self.iList[iType][instrument]["currency"],
1253                    self.iList[iType][instrument]["lot"],
1254                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1255                ))
1256
1257        infoText = "".join(info)
1258
1259        if show:
1260            uLogger.info(infoText)
1261
1262        if self.instrumentsFile:
1263            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1264                fH.write(infoText)
1265
1266            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1267
1268        return infoText
1269
1270    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1271        """
1272        This method search and show information about instruments by part of its ticker, FIGI or name.
1273        If `searchResultsFile` string is not empty then also save information to this file.
1274
1275        :param pattern: string with part of ticker, FIGI or instrument's name.
1276        :param show: if `True` then print results to console, if `False` - return list of result only.
1277        :return: list of dictionaries with all found instruments.
1278        """
1279        if not self.iList:
1280            self.iList = self.Listing()
1281
1282        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1283        compiledPattern = re.compile(pattern, re.IGNORECASE)
1284
1285        for iType in self.iList:
1286            for instrument in self.iList[iType].values():
1287                searchResult = compiledPattern.search(" ".join(
1288                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1289                ))
1290
1291                if searchResult:
1292                    searchResults[iType][instrument["ticker"]] = instrument
1293
1294        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1295        info = [
1296            "# Search results\n\n",
1297            "* **Search pattern:** [{}]\n".format(pattern),
1298            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1299            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1300        ]
1301        infoShort = info[:]
1302
1303        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1304        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1305        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1306
1307        if resultsLen == 0:
1308            info.append("\nNo results\n")
1309            infoShort.append("\nNo results\n")
1310            uLogger.warning("No results. Try changing your search pattern.")
1311
1312        else:
1313            for iType in searchResults:
1314                iTypeValuesCount = len(searchResults[iType].values())
1315                if iTypeValuesCount > 0:
1316                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1317                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1318
1319                    for instrument in searchResults[iType].values():
1320                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1321                            instrument["type"],
1322                            instrument["ticker"],
1323                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1324                            instrument["figi"],
1325                        ))
1326
1327                    if iTypeValuesCount <= 5:
1328                        infoShort.extend(info[-iTypeValuesCount:])
1329
1330                    else:
1331                        infoShort.extend(info[-5:])
1332                        infoShort.append(skippedLine)
1333
1334        infoText = "".join(info)
1335        infoTextShort = "".join(infoShort)
1336
1337        if show:
1338            uLogger.info(infoTextShort)
1339            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1340
1341        if self.searchResultsFile:
1342            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1343                fH.write(infoText)
1344
1345            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1346
1347        return searchResults
1348
1349    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1350        """
1351        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1352
1353        :param instruments: list of strings with tickers or FIGIs.
1354        :return: list with unique instrument FIGIs only.
1355        """
1356        requestedInstruments = []
1357        for iName in instruments:
1358            if iName not in self.aliases.keys():
1359                if iName not in requestedInstruments:
1360                    requestedInstruments.append(iName)
1361
1362            else:
1363                if iName not in requestedInstruments:
1364                    if self.aliases[iName] not in requestedInstruments:
1365                        requestedInstruments.append(self.aliases[iName])
1366
1367        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1368
1369        onlyUniqueFIGIs = []
1370        for iName in requestedInstruments:
1371            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1372                continue
1373
1374            self.ticker = iName
1375            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1376
1377            if not iData:
1378                self.ticker = ""
1379                self.figi = iName
1380
1381                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1382
1383                if not iData:
1384                    self.figi = ""
1385                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1386
1387            if iData and iData["figi"] not in onlyUniqueFIGIs:
1388                onlyUniqueFIGIs.append(iData["figi"])
1389
1390        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1391
1392        return onlyUniqueFIGIs
1393
1394    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1395        """
1396        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1397        See limits: https://tinkoff.github.io/investAPI/limits/
1398        If `pricesFile` string is not empty then also save information to this file.
1399
1400        :param instruments: list of strings with tickers or FIGIs.
1401        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1402        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1403                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1404        """
1405        if instruments is None or not instruments:
1406            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1407            raise Exception("Ticker or FIGI required")
1408
1409        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1410
1411        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1412
1413        iList = []  # trying to get info and current prices about all unique instruments:
1414        for self.figi in onlyUniqueFIGIs:
1415            iData = self.SearchByFIGI(requestPrice=True)
1416            iList.append(iData)
1417
1418        self.ShowListOfPrices(iList, show)
1419
1420        return iList
1421
1422    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1423        """
1424        Show table contains current prices of given instruments.
1425
1426        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1427                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1428        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1429        :return: multilines text in Markdown format as a table contains current prices.
1430        """
1431        infoText = ""
1432
1433        if show or self.pricesFile:
1434            info = [
1435                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1436                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1437                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1438            ]
1439
1440            for item in iList:
1441                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1442                    item["ticker"],
1443                    item["figi"],
1444                    item["type"],
1445                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1446                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1447                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1448                    "{} / {}".format(
1449                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1450                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1451                    ),
1452                    "{} / {}".format(
1453                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1454                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1455                    ),
1456                    item["currency"],
1457                ))
1458
1459            infoText = "".join(info)
1460
1461            if show:
1462                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1463
1464            if self.pricesFile:
1465                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1466                    fH.write(infoText)
1467
1468                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1469
1470        return infoText
1471
1472    def RequestTradingStatus(self) -> dict:
1473        """
1474        Requesting trading status for the instrument defined by `figi` variable.
1475        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1476        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1477
1478        :return: dictionary with trading status attributes. Response example:
1479                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1480                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1481        """
1482        if self.figi is None or not self.figi:
1483            uLogger.error("Variable `figi` must be defined for using this method!")
1484            raise Exception("FIGI required")
1485
1486        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1487
1488        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1489        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1490        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1491
1492        uLogger.debug("Records about current trading status successfully received")
1493
1494        return tradingStatus
1495
1496    def RequestPortfolio(self) -> dict:
1497        """
1498        Requesting actual user's portfolio for current `accountId`.
1499        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1500        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1501
1502        :return: dictionary with user's portfolio.
1503        """
1504        if self.accountId is None or not self.accountId:
1505            uLogger.error("Variable `accountId` must be defined for using this method!")
1506            raise Exception("Account ID required")
1507
1508        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1509
1510        self.body = str({"accountId": self.accountId})
1511        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1512        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1513
1514        uLogger.debug("Records about user's portfolio successfully received")
1515
1516        return rawPortfolio
1517
1518    def RequestPositions(self) -> dict:
1519        """
1520        Requesting open positions by currencies and instruments for current `accountId`.
1521        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1522        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1523
1524        :return: dictionary with open positions by instruments.
1525        """
1526        if self.accountId is None or not self.accountId:
1527            uLogger.error("Variable `accountId` must be defined for using this method!")
1528            raise Exception("Account ID required")
1529
1530        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1531
1532        self.body = str({"accountId": self.accountId})
1533        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1534        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1535
1536        uLogger.debug("Records about current open positions successfully received")
1537
1538        return rawPositions
1539
1540    def RequestPendingOrders(self) -> list:
1541        """
1542        Requesting current actual pending orders for current `accountId`.
1543        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1544        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1545
1546        :return: list of dictionaries with pending orders.
1547        """
1548        if self.accountId is None or not self.accountId:
1549            uLogger.error("Variable `accountId` must be defined for using this method!")
1550            raise Exception("Account ID required")
1551
1552        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1553
1554        self.body = str({"accountId": self.accountId})
1555        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1556        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1557
1558        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1559
1560        return rawOrders
1561
1562    def RequestStopOrders(self) -> list:
1563        """
1564        Requesting current actual stop orders for current `accountId`.
1565        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1566        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1567
1568        :return: list of dictionaries with stop orders.
1569        """
1570        if self.accountId is None or not self.accountId:
1571            uLogger.error("Variable `accountId` must be defined for using this method!")
1572            raise Exception("Account ID required")
1573
1574        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1575
1576        self.body = str({"accountId": self.accountId})
1577        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1578        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1579
1580        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1581
1582        return rawStopOrders
1583
1584    def Overview(self, show: bool = False, details: str = "full") -> dict:
1585        """
1586        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1587        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1588        are defined then also save information to file.
1589
1590        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1591        many requests about the state of the portfolio, and then, based on the received data, a large number
1592        of calculation and statistics are collected.
1593
1594        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1595        :param details: how detailed should the information be? You should specify one of strings:
1596                        `full` - shows full available information about portfolio status (by default),
1597                        `positions` - shows only open positions,
1598                        `digest` - show a short digest of the portfolio status,
1599                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1600                        `orders` - shows only sections of open limits and stop orders.
1601        :return: dictionary with client's raw portfolio and some statistics.
1602        """
1603        if self.accountId is None or not self.accountId:
1604            uLogger.error("Variable `accountId` must be defined for using this method!")
1605            raise Exception("Account ID required")
1606
1607        view = {
1608            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1609                "headers": {},  # list of dictionaries, response headers without "positions" section
1610                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1611                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1612                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1613                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1614                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1615                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1616                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1617                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1618                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1619            },
1620            "stat": {  # --- some statistics calculated using "raw" sections:
1621                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1622                "availableRUB": 0.,  # available rubles (without other currencies)
1623                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1624                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1625                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1626                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1627                "sharesCostRUB": 0.,  # costs of all shares in RUB
1628                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1629                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1630                "futuresCostRUB": 0.,  # costs of all futures in RUB
1631                "Currencies": [],  # list of dictionaries of all currencies statistics
1632                "Shares": [],  # list of dictionaries of all shares statistics
1633                "Bonds": [],  # list of dictionaries of all bonds statistics
1634                "Etfs": [],  # list of dictionaries of all etfs statistics
1635                "Futures": [],  # list of dictionaries of all futures statistics
1636                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1637                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1638                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1639                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1640                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1641            },
1642            "analytics": {  # --- some analytics of portfolio:
1643                "distrByAssets": {},  # portfolio distribution by assets
1644                "distrByCompanies": {},  # portfolio distribution by companies
1645                "distrBySectors": {},  # portfolio distribution by sectors
1646                "distrByCurrencies": {},  # portfolio distribution by currencies
1647                "distrByCountries": {},  # portfolio distribution by countries
1648            }
1649        }
1650
1651        details = details.lower()
1652        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1653        if details not in availableDetails:
1654            details = "full"
1655            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1656
1657        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1658
1659        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1660        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1661        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1662        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1663
1664        # save response headers without "positions" section:
1665        for key in portfolioResponse.keys():
1666            if key != "positions":
1667                view["raw"]["headers"][key] = portfolioResponse[key]
1668
1669            else:
1670                continue
1671
1672        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1673        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1674        for item in portfolioResponse["positions"]:
1675            if item["instrumentType"] == "currency":
1676                self.figi = item["figi"]
1677                curr = self.SearchByFIGI(requestPrice=False)
1678
1679                # current price of currency in RUB:
1680                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1681                    "name": curr["name"],
1682                    "currentPrice": NanoToFloat(
1683                        item["currentPrice"]["units"],
1684                        item["currentPrice"]["nano"]
1685                    ),
1686                }
1687
1688                view["raw"]["Currencies"].append(item)
1689
1690            elif item["instrumentType"] == "share":
1691                view["raw"]["Shares"].append(item)
1692
1693            elif item["instrumentType"] == "bond":
1694                view["raw"]["Bonds"].append(item)
1695
1696            elif item["instrumentType"] == "etf":
1697                view["raw"]["Etfs"].append(item)
1698
1699            elif item["instrumentType"] == "futures":
1700                view["raw"]["Futures"].append(item)
1701
1702            else:
1703                continue
1704
1705        # how many volume of currencies (by ISO currency name) are blocked:
1706        for item in view["raw"]["positions"]["blocked"]:
1707            blocked = NanoToFloat(item["units"], item["nano"])
1708            if blocked > 0:
1709                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1710
1711        # how many volume of instruments (by FIGI) are blocked:
1712        for item in view["raw"]["positions"]["securities"]:
1713            blocked = int(item["blocked"])
1714            if blocked > 0:
1715                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1716
1717        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1718
1719        if "rub" in allBlocked.keys():
1720            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1721
1722        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1723        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1724        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1725        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1726        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1727        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1728        view["stat"]["portfolioCostRUB"] = sum([
1729            view["stat"]["allCurrenciesCostRUB"],
1730            view["stat"]["sharesCostRUB"],
1731            view["stat"]["bondsCostRUB"],
1732            view["stat"]["etfsCostRUB"],
1733            view["stat"]["futuresCostRUB"],
1734        ])
1735
1736        # --- calculating some portfolio statistics:
1737        byComp = {}  # distribution by companies
1738        bySect = {}  # distribution by sectors
1739        byCurr = {}  # distribution by currencies (include RUB)
1740        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1741        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1742
1743        for item in portfolioResponse["positions"]:
1744            self.figi = item["figi"]
1745            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1746
1747            if instrument:
1748                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1749                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1750
1751                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1752                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1753
1754                else:
1755                    blocked = 0
1756
1757                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1758                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1759                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1760                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1761                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1762                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1763                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1764                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1765                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1766                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1767                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1768                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1769
1770                statData = {
1771                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1772                    "ticker": instrument["ticker"],  # ticker by FIGI
1773                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1774                    "volume": volume,  # available volume of instrument
1775                    "lots": lots,  # volume in lots of instrument
1776                    "direction": direction,  # direction of an instrument's position: short or long
1777                    "blocked": blocked,  # blocked volume of currency or instrument
1778                    "currentPrice": curPrice,  # current instrument's price in basic asset
1779                    "average": average,  # current average position price
1780                    "cost": cost,  # current cost of all volume of instrument in basic asset
1781                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1782                    "costRUB": costRUB,  # cost of instrument in ruble
1783                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1784                    "profit": profit,  # expected profit at current moment
1785                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1786                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1787                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1788                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1789                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1790                    "step": instrument["step"],  # minimum price increment
1791                }
1792
1793                # adding distribution by unique countries:
1794                if statData["country"] not in byCountry.keys():
1795                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1796
1797                else:
1798                    byCountry[statData["country"]]["cost"] += costRUB
1799                    byCountry[statData["country"]]["percent"] += percentCostRUB
1800
1801                if item["instrumentType"] != "currency":
1802                    # adding distribution by unique companies:
1803                    if statData["name"]:
1804                        if statData["name"] not in byComp.keys():
1805                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1806
1807                        else:
1808                            byComp[statData["name"]]["cost"] += costRUB
1809                            byComp[statData["name"]]["percent"] += percentCostRUB
1810
1811                    # adding distribution by unique sectors:
1812                    if statData["sector"] not in bySect.keys():
1813                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1814
1815                    else:
1816                        bySect[statData["sector"]]["cost"] += costRUB
1817                        bySect[statData["sector"]]["percent"] += percentCostRUB
1818
1819                # adding distribution by unique currencies:
1820                if currency not in byCurr.keys():
1821                    byCurr[currency] = {
1822                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1823                        "cost": costRUB,
1824                        "percent": percentCostRUB
1825                    }
1826
1827                else:
1828                    byCurr[currency]["cost"] += costRUB
1829                    byCurr[currency]["percent"] += percentCostRUB
1830
1831                # saving statistics for every instrument:
1832                if item["instrumentType"] == "currency":
1833                    view["stat"]["Currencies"].append(statData)
1834
1835                    # update dict with free funds for trading (total - blocked) by currencies
1836                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1837                    view["stat"]["funds"][currency] = {
1838                        "total": volume,
1839                        "totalCostRUB": costRUB,  # total volume cost in rubles
1840                        "free": volume - blocked,
1841                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1842                    }
1843
1844                elif item["instrumentType"] == "share":
1845                    view["stat"]["Shares"].append(statData)
1846
1847                elif item["instrumentType"] == "bond":
1848                    view["stat"]["Bonds"].append(statData)
1849
1850                elif item["instrumentType"] == "etf":
1851                    view["stat"]["Etfs"].append(statData)
1852
1853                elif item["instrumentType"] == "Futures":
1854                    view["stat"]["Futures"].append(statData)
1855
1856                else:
1857                    continue
1858
1859        # total changes in Russian Ruble:
1860        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1861        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1862        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1863        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1864        view["stat"]["funds"]["rub"] = {
1865            "total": view["stat"]["availableRUB"],
1866            "totalCostRUB": view["stat"]["availableRUB"],
1867            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1868            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1869        }
1870
1871        # --- pending orders sector data:
1872        uniquePendingOrders = []
1873        uniquePendingOrdersFIGIs = []
1874        for item in view["raw"]["orders"]:
1875            if item["figi"] not in uniquePendingOrdersFIGIs:
1876                uniquePendingOrdersFIGIs.append(item["figi"])
1877                uniquePendingOrders.append(item)
1878
1879        for item in uniquePendingOrders:
1880            self.figi = item["figi"]
1881            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1882
1883            if instrument:
1884                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1885                orderType = TKS_ORDER_TYPES[item["orderType"]]
1886                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1887                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1888
1889                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1890                if item["direction"] == "ORDER_DIRECTION_BUY":
1891                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1892
1893                else:
1894                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1895
1896                # requested price for order execution:
1897                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1898
1899                # necessary changes in percent to reach target from current price:
1900                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1901
1902                view["stat"]["orders"].append({
1903                    "orderID": item["orderId"],  # orderId number parameter of current order
1904                    "figi": item["figi"],  # FIGI identification
1905                    "ticker": instrument["ticker"],  # ticker name by FIGI
1906                    "lotsRequested": item["lotsRequested"],  # requested lots value
1907                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1908                    "currentPrice": lastPrice,  # current instrument's price for defined action
1909                    "targetPrice": target,  # requested price for order execution in base currency
1910                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1911                    "percentChanges": changes,  # changes in percent to target from current price
1912                    "currency": item["currency"],  # instrument's currency name
1913                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1914                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1915                    "status": orderState,  # order status from TKS_ORDER_STATES
1916                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1917                })
1918
1919        # --- stop orders sector data:
1920        uniqueStopOrders = []
1921        uniqueStopOrdersFIGIs = []
1922        for item in view["raw"]["stopOrders"]:
1923            if item["figi"] not in uniqueStopOrdersFIGIs:
1924                uniqueStopOrdersFIGIs.append(item["figi"])
1925                uniqueStopOrders.append(item)
1926
1927        for item in uniqueStopOrders:
1928            self.figi = item["figi"]
1929            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1930
1931            if instrument:
1932                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1933                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1934                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1935
1936                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1937                if "expirationTime" in item.keys():
1938                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1939                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1940
1941                else:
1942                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1943                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1944
1945                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1946                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1947                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1948
1949                else:
1950                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1951
1952                # requested price when stop-order executed:
1953                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1954
1955                # price for limit-order, set up when stop-order executed:
1956                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1957
1958                # necessary changes in percent to reach target from current price:
1959                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1960
1961                view["stat"]["stopOrders"].append({
1962                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1963                    "figi": item["figi"],  # FIGI identification
1964                    "ticker": instrument["ticker"],  # ticker name by FIGI
1965                    "lotsRequested": item["lotsRequested"],  # requested lots value
1966                    "currentPrice": lastPrice,  # current instrument's price for defined action
1967                    "targetPrice": target,  # requested price for stop-order execution in base currency
1968                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1969                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1970                    "percentChanges": changes,  # changes in percent to target from current price
1971                    "currency": item["currency"],  # instrument's currency name
1972                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1973                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1974                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1975                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1976                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1977                })
1978
1979        # --- calculating data for analytics section:
1980        # portfolio distribution by assets:
1981        view["analytics"]["distrByAssets"] = {
1982            "Ruble": {
1983                "uniques": 1,
1984                "cost": view["stat"]["availableRUB"],
1985                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1986            },
1987            "Currencies": {
1988                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1989                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1990                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1991            },
1992            "Shares": {
1993                "uniques": len(view["stat"]["Shares"]),
1994                "cost": view["stat"]["sharesCostRUB"],
1995                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1996            },
1997            "Bonds": {
1998                "uniques": len(view["stat"]["Bonds"]),
1999                "cost": view["stat"]["bondsCostRUB"],
2000                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2001            },
2002            "Etfs": {
2003                "uniques": len(view["stat"]["Etfs"]),
2004                "cost": view["stat"]["etfsCostRUB"],
2005                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2006            },
2007            "Futures": {
2008                "uniques": len(view["stat"]["Futures"]),
2009                "cost": view["stat"]["futuresCostRUB"],
2010                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2011            },
2012        }
2013
2014        # portfolio distribution by companies:
2015        view["analytics"]["distrByCompanies"]["All money cash"] = {
2016            "ticker": "",
2017            "cost": view["stat"]["allCurrenciesCostRUB"],
2018            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2019        }
2020        view["analytics"]["distrByCompanies"].update(byComp)
2021
2022        # portfolio distribution by sectors:
2023        view["analytics"]["distrBySectors"]["All money cash"] = {
2024            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2025            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2026        }
2027        view["analytics"]["distrBySectors"].update(bySect)
2028
2029        # portfolio distribution by currencies:
2030        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2031            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2032            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2033
2034        view["analytics"]["distrByCurrencies"].update(byCurr)
2035        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2036        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2037
2038        # portfolio distribution by countries:
2039        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2040            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2041            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2042
2043        view["analytics"]["distrByCountries"].update(byCountry)
2044        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2045        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2046
2047        # --- Prepare text statistics overview in human-readable:
2048        if show:
2049            # Whatever the value `details`, header not changes:
2050            info = [
2051                "# Client's portfolio\n\n",
2052                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2053                "* **Account ID:** [{}]\n".format(self.accountId),
2054            ]
2055
2056            if details in ["full", "positions", "digest"]:
2057                info.extend([
2058                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2059                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2060                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2061                        view["stat"]["totalChangesRUB"],
2062                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2063                        view["stat"]["totalChangesPercentRUB"],
2064                    ),
2065                ])
2066
2067            if details in ["full", "positions"]:
2068                info.extend([
2069                    "## Open positions\n\n",
2070                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2071                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2072                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2073                        "{:.2f} ({:.2f}) rub".format(
2074                            view["stat"]["availableRUB"],
2075                            view["stat"]["blockedRUB"],
2076                        )
2077                    )
2078                ])
2079
2080                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2081                    return [
2082                        "|                             |                                 |          |              |              |                     |                              |\n",
2083                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2084                            noTradeStr if noTradeStr else typeStr,
2085                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2086                        ),
2087                    ]
2088
2089                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2090                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2091                        "{} [{}]".format(data["ticker"], data["figi"]),
2092                        "{:.2f} ({:.2f}) {}".format(
2093                            data["volume"],
2094                            data["blocked"],
2095                            data["currency"],
2096                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2097                            data["volume"],
2098                            data["blocked"],
2099                        ),
2100                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2101                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2102                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2103                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2104                        "{}{:.2f} {} ({}{:.2f}%)".format(
2105                            "+" if data["profit"] > 0 else "",
2106                            data["profit"], data["baseCurrencyName"],
2107                            "+" if data["percentProfit"] > 0 else "",
2108                            data["percentProfit"],
2109                        ),
2110                    )
2111
2112                # --- Show currencies section:
2113                if view["stat"]["Currencies"]:
2114                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2115                    for item in view["stat"]["Currencies"]:
2116                        info.append(_InfoStr(item, showCurrencyName=True))
2117
2118                else:
2119                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2120
2121                # --- Show shares section:
2122                if view["stat"]["Shares"]:
2123                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2124
2125                    for item in view["stat"]["Shares"]:
2126                        info.append(_InfoStr(item))
2127
2128                else:
2129                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2130
2131                # --- Show bonds section:
2132                if view["stat"]["Bonds"]:
2133                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2134
2135                    for item in view["stat"]["Bonds"]:
2136                        info.append(_InfoStr(item))
2137
2138                else:
2139                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2140
2141                # --- Show etfs section:
2142                if view["stat"]["Etfs"]:
2143                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2144
2145                    for item in view["stat"]["Etfs"]:
2146                        info.append(_InfoStr(item))
2147
2148                else:
2149                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2150
2151                # --- Show futures section:
2152                if view["stat"]["Futures"]:
2153                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2154
2155                    for item in view["stat"]["Futures"]:
2156                        info.append(_InfoStr(item))
2157
2158                else:
2159                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2160
2161            if details in ["full", "orders"]:
2162                # --- Show pending orders section:
2163                if view["stat"]["orders"]:
2164                    info.extend([
2165                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2166                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2167                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2168                    ])
2169
2170                    for item in view["stat"]["orders"]:
2171                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2172                            "{} [{}]".format(item["ticker"], item["figi"]),
2173                            item["orderID"],
2174                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2175                            "{} {} ({}{:.2f}%)".format(
2176                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2177                                item["baseCurrencyName"],
2178                                "+" if item["percentChanges"] > 0 else "",
2179                                float(item["percentChanges"]),
2180                            ),
2181                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2182                            item["action"],
2183                            item["type"],
2184                            item["date"],
2185                        ))
2186
2187                else:
2188                    info.append("\n## Total pending limit-orders: 0\n")
2189
2190                # --- Show stop orders section:
2191                if view["stat"]["stopOrders"]:
2192                    info.extend([
2193                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2194                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2195                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2196                    ])
2197
2198                    for item in view["stat"]["stopOrders"]:
2199                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2200                            "{} [{}]".format(item["ticker"], item["figi"]),
2201                            item["orderID"],
2202                            item["lotsRequested"],
2203                            "{} {} ({}{:.2f}%)".format(
2204                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2205                                item["baseCurrencyName"],
2206                                "+" if item["percentChanges"] > 0 else "",
2207                                float(item["percentChanges"]),
2208                            ),
2209                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2210                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2211                            item["action"],
2212                            item["type"],
2213                            item["expType"],
2214                            item["createDate"],
2215                            item["expDate"],
2216                        ))
2217
2218                else:
2219                    info.append("\n## Total stop-orders: 0\n")
2220
2221            if details in ["full", "analytics"]:
2222                # -- Show analytics section:
2223                if view["stat"]["portfolioCostRUB"] > 0:
2224                    info.extend([
2225                        "\n# Analytics\n"
2226                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2227                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2228                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2229                            view["stat"]["totalChangesRUB"],
2230                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2231                            view["stat"]["totalChangesPercentRUB"],
2232                        ),
2233                        "\n## Portfolio distribution by assets\n"
2234                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2235                        "|------------|---------|---------|--------------------|\n",
2236                    ])
2237
2238                    for key in view["analytics"]["distrByAssets"].keys():
2239                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2240                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2241                                key,
2242                                view["analytics"]["distrByAssets"][key]["uniques"],
2243                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2244                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2245                            ))
2246
2247                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2248                    info.extend([
2249                        "\n## Portfolio distribution by companies\n"
2250                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2251                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2252                    ])
2253
2254                    for company in view["analytics"]["distrByCompanies"].keys():
2255                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2256                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2257                            info.append("| {} | {:<7} | {:<18} |\n".format(
2258                                "{}{}{}".format(
2259                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2260                                    company,
2261                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2262                                ),
2263                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2264                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2265                            ))
2266
2267                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2268                    info.extend([
2269                        "\n## Portfolio distribution by sectors\n"
2270                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2271                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2272                    ])
2273
2274                    for sector in view["analytics"]["distrBySectors"].keys():
2275                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2276                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2277                                sector,
2278                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2279                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2280                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2281                            ))
2282
2283                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2284                    info.extend([
2285                        "\n## Portfolio distribution by currencies\n"
2286                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2287                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2288                    ])
2289
2290                    for curr in view["analytics"]["distrByCurrencies"].keys():
2291                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2292                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2293                            info.append("| {} | {:<7} | {:<18} |\n".format(
2294                                "[{}] {}{}".format(
2295                                    curr,
2296                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2297                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2298                                ),
2299                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2300                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2301                            ))
2302
2303                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2304                    info.extend([
2305                        "\n## Portfolio distribution by countries\n"
2306                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2307                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2308                    ])
2309
2310                    for country in view["analytics"]["distrByCountries"].keys():
2311                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2312                            nameLen = len(country)
2313                            info.append("| {} | {:<7} | {:<18} |\n".format(
2314                                "{}{}".format(
2315                                    country,
2316                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2317                                ),
2318                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2319                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2320                            ))
2321
2322            infoText = "".join(info)
2323
2324            uLogger.info(infoText)
2325
2326            if details == "full" and self.overviewFile:
2327                filename = self.overviewFile
2328
2329            elif details == "digest" and self.overviewDigestFile:
2330                filename = self.overviewDigestFile
2331
2332            elif details == "positions" and self.overviewPositionsFile:
2333                filename = self.overviewPositionsFile
2334
2335            elif details == "orders" and self.overviewOrdersFile:
2336                filename = self.overviewOrdersFile
2337
2338            elif details == "analytics" and self.overviewAnalyticsFile:
2339                filename = self.overviewAnalyticsFile
2340
2341            else:
2342                filename = ""
2343
2344            if filename:
2345                with open(filename, "w", encoding="UTF-8") as fH:
2346                    fH.write(infoText)
2347
2348                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2349
2350        return view
2351
2352    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2353        """
2354        Returns history operations between two given dates for current `accountId`.
2355        If `reportFile` string is not empty then also save human-readable report.
2356        Shows some statistical data of closed positions.
2357
2358        :param start: see docstring in `GetDatesAsString()` method
2359        :param end: see docstring in `GetDatesAsString()` method
2360        :param show: if `True` then also prints all records to the console.
2361        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2362        :return: original list of dictionaries with history of deals records from API ("operations" key):
2363                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2364                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2365        """
2366        if self.accountId is None or not self.accountId:
2367            uLogger.error("Variable `accountId` must be defined for using this method!")
2368            raise Exception("Account ID required")
2369
2370        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2371
2372        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2373
2374        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2375        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2376        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2377        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2378        customStat = {}  # custom statistics in additional to responseJSON
2379
2380        # --- output report in human-readable format:
2381        if show or self.reportFile:
2382            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2383            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2384            nextDay = ""
2385
2386            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2387
2388            if len(ops) > 0:
2389                customStat = {
2390                    "opsCount": 0,  # total operations count
2391                    "buyCount": 0,  # buy operations
2392                    "sellCount": 0,  # sell operations
2393                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2394                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2395                    "payIn": {"rub": 0.},  # Deposit brokerage account
2396                    "payOut": {"rub": 0.},  # Withdrawals
2397                    "divs": {"rub": 0.},  # Dividends income
2398                    "coupons": {"rub": 0.},  # Coupon's income
2399                    "brokerCom": {"rub": 0.},  # Service commissions
2400                    "serviceCom": {"rub": 0.},  # Service commissions
2401                    "marginCom": {"rub": 0.},  # Margin commissions
2402                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2403                }
2404
2405                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2406                for item in ops:
2407                    if item["state"] == "OPERATION_STATE_EXECUTED":
2408                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2409
2410                        # count buy operations:
2411                        if "_BUY" in item["operationType"]:
2412                            customStat["buyCount"] += 1
2413
2414                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2415                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2416
2417                            else:
2418                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2419
2420                        # count sell operations:
2421                        elif "_SELL" in item["operationType"]:
2422                            customStat["sellCount"] += 1
2423
2424                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2425                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2426
2427                            else:
2428                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2429
2430                        # count incoming operations:
2431                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2432                            if item["payment"]["currency"] in customStat["payIn"].keys():
2433                                customStat["payIn"][item["payment"]["currency"]] += payment
2434
2435                            else:
2436                                customStat["payIn"][item["payment"]["currency"]] = payment
2437
2438                        # count withdrawals operations:
2439                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2440                            if item["payment"]["currency"] in customStat["payOut"].keys():
2441                                customStat["payOut"][item["payment"]["currency"]] += payment
2442
2443                            else:
2444                                customStat["payOut"][item["payment"]["currency"]] = payment
2445
2446                        # count dividends income:
2447                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2448                            if item["payment"]["currency"] in customStat["divs"].keys():
2449                                customStat["divs"][item["payment"]["currency"]] += payment
2450
2451                            else:
2452                                customStat["divs"][item["payment"]["currency"]] = payment
2453
2454                        # count coupon's income:
2455                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2456                            if item["payment"]["currency"] in customStat["coupons"].keys():
2457                                customStat["coupons"][item["payment"]["currency"]] += payment
2458
2459                            else:
2460                                customStat["coupons"][item["payment"]["currency"]] = payment
2461
2462                        # count broker commissions:
2463                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2464                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2465                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2466
2467                            else:
2468                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2469
2470                        # count service commissions:
2471                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2472                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2473                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2474
2475                            else:
2476                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2477
2478                        # count margin commissions:
2479                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2480                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2481                                customStat["marginCom"][item["payment"]["currency"]] += payment
2482
2483                            else:
2484                                customStat["marginCom"][item["payment"]["currency"]] = payment
2485
2486                        # count withholding taxes:
2487                        elif "_TAX" in item["operationType"]:
2488                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2489                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2490
2491                            else:
2492                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2493
2494                        else:
2495                            continue
2496
2497                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2498
2499                # --- view "Actions" lines:
2500                info.extend([
2501                    "| 1                          | 2                             | 3                            | 4                    | 5                      |\n",
2502                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2503                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2504                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2505                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2506                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2507                    ),
2508                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2509                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2510                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2511                    ),
2512                ])
2513
2514                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2515                for key in opsKeys:
2516                    if key == "rub":
2517                        continue
2518
2519                    info.extend([
2520                        "|                            |                               | {:<28} |                      |                        |\n".format(
2521                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2522                        ),
2523                        "|                            |                               | {:<28} |                      |                        |\n".format(
2524                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2525                        ),
2526                    ])
2527
2528                info.append(splitLine1)
2529
2530                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2531                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2532                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2533                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2534                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2535                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2536                    )
2537
2538                # --- view "Payments" lines:
2539                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2540                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2541
2542                for key in paymentsKeys:
2543                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2544
2545                info.append(splitLine1)
2546
2547                # --- view "Commissions and taxes" lines:
2548                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2549                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2550
2551                for key in comKeys:
2552                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2553
2554                info.append(splitLine1)
2555
2556                info.extend([
2557                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2558                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2559                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2560                ])
2561
2562            else:
2563                info.append("Broker returned no operations during this period\n")
2564
2565            # --- view "Operations" section:
2566            for item in ops:
2567                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2568                    continue
2569
2570                else:
2571                    self.figi = item["figi"] if item["figi"] else ""
2572                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2573                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2574
2575                    # group of deals during one day:
2576                    if nextDay and item["date"].split("T")[0] != nextDay:
2577                        info.append(splitLine2)
2578                        nextDay = ""
2579
2580                    else:
2581                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2582
2583                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2584                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2585                        self.figi if self.figi else "—",
2586                        instrument["ticker"] if instrument else "—",
2587                        instrument["type"] if instrument else "—",
2588                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2589                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2590                        TKS_OPERATION_STATES[item["state"]],
2591                        TKS_OPERATION_TYPES[item["operationType"]],
2592                    ))
2593
2594            infoText = "".join(info)
2595
2596            if show:
2597                uLogger.info(infoText)
2598
2599            if self.reportFile:
2600                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2601                    fH.write(infoText)
2602
2603                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2604
2605        return ops, customStat
2606
2607    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2608        """
2609        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2610
2611        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2612        Warning! Broker server used ISO UTC time by default.
2613
2614        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2615        Also, `historyFile` used to update history with `onlyMissing` parameter.
2616
2617        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2618
2619        :param start: see docstring in `GetDatesAsString()` method.
2620        :param end: see docstring in `GetDatesAsString()` method.
2621        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2622                         `"hour"`, `"day"`. Default: `"hour"`.
2623        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2624                            False by default. Warning! History appends only from last candle to current time
2625                            with always update last candle!
2626        :param csvSep: separator if csv-file is used, `,` by default.
2627        :param show: if `True` then also prints Pandas DataFrame to the console.
2628        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2629                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2630        """
2631        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2632        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2633        history = None  # empty pandas object for history
2634
2635        if interval not in TKS_CANDLE_INTERVALS.keys():
2636            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2637            raise Exception("Incorrect value")
2638
2639        if not (self.ticker or self.figi):
2640            uLogger.error("Ticker or FIGI must be defined!")
2641            raise Exception("Ticker or FIGI required")
2642
2643        if self.ticker and not self.figi:
2644            instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False)
2645            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2646
2647        if self.figi and not self.ticker:
2648            instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False)
2649            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2650
2651        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2652        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2653        if interval.lower() != "day":
2654            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2655
2656        delta = dtEnd - dtStart  # current UTC time minus last time in file
2657        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2658
2659        # calculate history length in candles:
2660        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2661        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2662            length += 1  # to avoid fraction time
2663
2664        # calculate data blocks count:
2665        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2666
2667        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2668        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2669        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2670        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2671        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2672
2673        tempOld = None  # pandas object for old history, if --only-missing key present
2674        lastTime = None  # datetime object of last old candle in file
2675
2676        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2677            uLogger.debug("--only-missing key present, add only last missing candles...")
2678            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2679
2680            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2681
2682            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2683            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2684            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2685            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2686
2687            # get last datetime object from last string in file or minus 1 delta if file is empty:
2688            if len(tempOld) > 0:
2689                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2690
2691            else:
2692                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2693
2694            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2695
2696        responseJSONs = []  # raw history blocks of data
2697
2698        blockEnd = dtEnd
2699        for item in range(blocks):
2700            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2701            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2702
2703            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2704                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2705            ))
2706
2707            if blockStart == blockEnd:
2708                uLogger.debug("Skipped this zero-length block...")
2709
2710            else:
2711                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2712                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2713                self.body = str({
2714                    "figi": self.figi,
2715                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2716                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2717                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2718                })
2719                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False)
2720
2721                if "code" in responseJSON.keys():
2722                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2723
2724                else:
2725                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2726                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2727
2728                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2729
2730            blockEnd = blockStart
2731
2732        printCount = len(responseJSONs)  # candles to show in console
2733        if responseJSONs:
2734            tempHistory = pd.DataFrame(
2735                data={
2736                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2737                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2738                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2739                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2740                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2741                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2742                    "volume": [int(item["volume"]) for item in responseJSONs],
2743                },
2744                index=range(len(responseJSONs)),
2745                columns=["date", "time", "open", "high", "low", "close", "volume"],
2746            )
2747            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2748            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2749
2750            # append only newest candles to old history if --only-missing key present:
2751            if onlyMissing and tempOld is not None and lastTime is not None:
2752                index = 0  # find start index in tempHistory data:
2753
2754                for i, item in tempHistory.iterrows():
2755                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2756
2757                    if curTime == lastTime:
2758                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2759                        index = i
2760                        printCount = index + 1
2761                        break
2762
2763                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2764
2765            else:
2766                history = tempHistory  # if no `--only-missing` key then load full data from server
2767
2768            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2769
2770        if history is not None and not history.empty:
2771            if show:
2772                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2773                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2774                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2775                ))
2776
2777        else:
2778            uLogger.warning("Received an empty candles history!")
2779
2780        if self.historyFile is not None:
2781            if history is not None and not history.empty:
2782                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2783                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2784
2785            else:
2786                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2787
2788        else:
2789            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2790
2791        return history
2792
2793    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2794        """
2795        Load candles history from csv-file and return Pandas DataFrame object.
2796
2797        See also: `History()` and `ShowHistoryChart()` methods.
2798
2799        :param filePath: path to csv-file to open.
2800        """
2801        loadedHistory = None  # init candles data object
2802
2803        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2804
2805        if os.path.exists(filePath):
2806            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2807
2808            tfStr = self.priceModel.FormattedDelta(
2809                self.priceModel.timeframe,
2810                "{days} days {hours}h {minutes}m {seconds}s",
2811            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2812                self.priceModel.timeframe,
2813                "{hours}h {minutes}m {seconds}s",
2814            )
2815
2816            if loadedHistory is not None and not loadedHistory.empty:
2817                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2818                    len(loadedHistory),
2819                    tfStr,
2820                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2821                )
2822
2823            else:
2824                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2825
2826        else:
2827            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2828
2829        return loadedHistory
2830
2831    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2832        """
2833        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2834
2835        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2836        Default: `index.html` (both for interact and non-interact candlesticks chart).
2837
2838        See also: `History()` and `LoadHistory()` methods.
2839
2840        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2841        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2842                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2843                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2844                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2845        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2846                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2847        """
2848        if isinstance(candles, str):
2849            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2850            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2851
2852        elif isinstance(candles, pd.DataFrame):
2853            self.priceModel.prices = candles  # set candles chain from variable
2854            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2855
2856            if "datetime" not in candles.columns:
2857                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2858
2859        else:
2860            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2861            raise Exception("Incorrect value")
2862
2863        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2864
2865        if interact:
2866            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2867
2868            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2869
2870        else:
2871            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2872
2873            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2874
2875        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2876
2877    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2878        """
2879        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2880        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2881
2882        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2883
2884        :param operation: string "Buy" or "Sell".
2885        :param lots: volume, integer count of lots >= 1.
2886        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2887        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2888        :param expDate: string "Undefined" by default or local date in future,
2889                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2890        :return: JSON with response from broker server.
2891        """
2892        if self.accountId is None or not self.accountId:
2893            uLogger.error("Variable `accountId` must be defined for using this method!")
2894            raise Exception("Account ID required")
2895
2896        if operation is None or not operation or operation not in ("Buy", "Sell"):
2897            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2898            raise Exception("Incorrect value")
2899
2900        if lots is None or lots < 1:
2901            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2902            lots = 1
2903
2904        if tp is None or tp < 0:
2905            tp = 0
2906
2907        if sl is None or sl < 0:
2908            sl = 0
2909
2910        if expDate is None or not expDate:
2911            expDate = "Undefined"
2912
2913        if not (self.ticker or self.figi):
2914            uLogger.error("Ticker or FIGI must be defined!")
2915            raise Exception("Ticker or FIGI required")
2916
2917        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
2918        self.ticker = instrument["ticker"]
2919        self.figi = instrument["figi"]
2920
2921        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2922
2923        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2924        self.body = str({
2925            "figi": self.figi,
2926            "quantity": str(lots),
2927            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2928            "accountId": str(self.accountId),
2929            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2930        })
2931        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False)
2932
2933        if "orderId" in response.keys():
2934            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2935                operation, response["orderId"],
2936                self.ticker, self.figi, lots,
2937                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2938                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2939                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2940            ))
2941
2942        else:
2943            uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.")
2944
2945        if tp > 0:
2946            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2947
2948        if sl > 0:
2949            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2950
2951        return response
2952
2953    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2954        """
2955        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2956        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2957
2958        See also: `Order()` and `Trade()` docstrings.
2959
2960        :param lots: volume, integer count of lots >= 1.
2961        :param tp: float > 0, take profit price of stop-order.
2962        :param sl: float > 0, stop loss price of stop-order.
2963        :param expDate: it's a local date in future.
2964                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2965        :return: JSON with response from broker server.
2966        """
2967        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2968
2969    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2970        """
2971        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2972        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2973
2974        See also: `Order()` and `Trade()` docstrings.
2975
2976        :param lots: volume, integer count of lots >= 1.
2977        :param tp: float > 0, take profit price of stop-order.
2978        :param sl: float > 0, stop loss price of stop-order.
2979        :param expDate: it's a local date in the future.
2980                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2981        :return: JSON with response from broker server.
2982        """
2983        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2984
2985    def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2986        """
2987        Close position of given instruments.
2988
2989        :param tickers: tickers list of instruments that must be closed.
2990        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2991                         This avoids unnecessary downloading data from the server.
2992        """
2993        if not tickers:
2994            uLogger.info("Tickers list is empty, nothing to close.")
2995
2996        else:
2997            if portfolio is None or not portfolio:
2998                portfolio = self.Overview(show=False)
2999
3000            allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3001            uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers))
3002
3003            for ticker in tickers:
3004                if ticker not in allOpenedTickers:
3005                    uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker))
3006                    continue
3007
3008                # search open trade info about instrument by ticker:
3009                instrument = {}
3010                for iType in TKS_INSTRUMENTS:
3011                    if instrument:
3012                        break
3013
3014                    for item in portfolio["stat"][iType]:
3015                        if item["ticker"] == ticker:
3016                            instrument = item
3017                            break
3018
3019                if instrument:
3020                    self.ticker = ticker
3021                    self.figi = instrument["figi"]
3022
3023                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3024                        self.ticker,
3025                        self.figi,
3026                        int(instrument["volume"]),
3027                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3028                    ))
3029
3030                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3031
3032                    if tradeLots > 0:
3033                        if instrument["blocked"] > 0:
3034                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3035                                instrument["blocked"],
3036                                self.ticker,
3037                                tradeLots,
3038                            ))
3039
3040                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3041                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3042
3043                    else:
3044                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3045
3046    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3047        """
3048        Close all positions of given instruments with defined type.
3049
3050        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3051        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3052                         This avoids unnecessary downloading data from the server.
3053        """
3054        if iType not in TKS_INSTRUMENTS:
3055            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3056
3057        else:
3058            if portfolio is None or not portfolio:
3059                portfolio = self.Overview(show=False)
3060
3061            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3062            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3063
3064            if tickers and portfolio:
3065                self.CloseTrades(tickers, portfolio)
3066
3067            else:
3068                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3069
3070    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3071        """
3072        Universal method to create market or limit orders with all available parameters for current `accountId`.
3073        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3074
3075        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3076        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3077
3078        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3079        then broker immediately open market order as you can do simple --buy or --sell operations!
3080
3081        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3082        When current price will go up or down to target price value then broker opens a limit order.
3083        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3084
3085        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3086
3087        :param operation: string "Buy" or "Sell".
3088        :param orderType: string "Limit" or "Stop".
3089        :param lots: volume, integer count of lots >= 1.
3090        :param targetPrice: target price > 0. This is open trade price for limit order.
3091        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3092                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3093        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3094                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3095                         Stop loss order always executed by market price.
3096        :param expDate: string "Undefined" by default or local date in future.
3097                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3098                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3099                        A limit order has no expiration date, it lasts until the end of the trading day.
3100        :return: JSON with response from broker server.
3101        """
3102        if self.accountId is None or not self.accountId:
3103            uLogger.error("Variable `accountId` must be defined for using this method!")
3104            raise Exception("Account ID required")
3105
3106        if operation is None or not operation or operation not in ("Buy", "Sell"):
3107            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3108            raise Exception("Incorrect value")
3109
3110        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3111            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3112            raise Exception("Incorrect value")
3113
3114        if lots is None or lots < 1:
3115            uLogger.error("You must define trade volume > 0: integer count of lots!")
3116            raise Exception("Incorrect value")
3117
3118        if targetPrice is None or targetPrice <= 0:
3119            uLogger.error("Target price for limit-order must be greater than 0!")
3120            raise Exception("Incorrect value")
3121
3122        if limitPrice is None or limitPrice <= 0:
3123            limitPrice = targetPrice
3124
3125        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3126            stopType = "Limit"
3127
3128        if expDate is None or not expDate:
3129            expDate = "Undefined"
3130
3131        if not (self.ticker or self.figi):
3132            uLogger.error("Tocker or FIGI must be defined!")
3133            raise Exception("Ticker or FIGI required")
3134
3135        response = {}
3136        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
3137        self.ticker = instrument["ticker"]
3138        self.figi = instrument["figi"]
3139
3140        if orderType == "Limit":
3141            uLogger.debug(
3142                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3143                    self.ticker, self.figi,
3144                    operation, lots, targetPrice, instrument["currency"],
3145                ))
3146
3147            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3148            self.body = str({
3149                "figi": self.figi,
3150                "quantity": str(lots),
3151                "price": FloatToNano(targetPrice),
3152                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3153                "accountId": str(self.accountId),
3154                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3155            })
3156            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3157
3158            if "orderId" in response.keys():
3159                uLogger.info(
3160                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3161                        response["orderId"],
3162                        self.ticker, self.figi,
3163                        operation, lots, targetPrice, instrument["currency"],
3164                    ))
3165
3166                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3167                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3168                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3169                            targetPrice, instrument["currency"],
3170                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3171                        ))
3172
3173                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3174                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3175                            targetPrice, instrument["currency"],
3176                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3177                        ))
3178
3179            else:
3180                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3181
3182        if orderType == "Stop":
3183            uLogger.debug(
3184                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3185                    self.ticker, self.figi,
3186                    operation, lots,
3187                    targetPrice, instrument["currency"],
3188                    limitPrice, instrument["currency"],
3189                    stopType, expDate,
3190                ))
3191
3192            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3193            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3194            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3195
3196            body = {
3197                "figi": self.figi,
3198                "quantity": str(lots),
3199                "price": FloatToNano(limitPrice),
3200                "stopPrice": FloatToNano(targetPrice),
3201                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3202                "accountId": str(self.accountId),
3203                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3204                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3205            }
3206
3207            if expDateUTC:
3208                body["expireDate"] = expDateUTC
3209
3210            self.body = str(body)
3211            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3212
3213            if "stopOrderId" in response.keys():
3214                uLogger.info(
3215                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3216                        response["stopOrderId"],
3217                        self.ticker, self.figi,
3218                        operation, lots,
3219                        targetPrice, instrument["currency"],
3220                        limitPrice, instrument["currency"],
3221                        TKS_STOP_ORDER_TYPES[stopOrderType],
3222                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3223                    ))
3224
3225                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3226                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3227                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3228                            targetPrice, instrument["currency"],
3229                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3230                        ))
3231
3232                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3233                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3234                            targetPrice, instrument["currency"],
3235                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3236                        ))
3237
3238            else:
3239                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3240
3241        return response
3242
3243    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3244        """
3245        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3246        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3247        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3248        See also: `Order()` docstring.
3249
3250        :param lots: volume, integer count of lots >= 1.
3251        :param targetPrice: target price > 0. This is open trade price for limit order.
3252        :return: JSON with response from broker server.
3253        """
3254        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3255
3256    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3257        """
3258        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3259        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3260        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3261        target price value then broker opens a limit order. See also: `Order()` docstring.
3262
3263        :param lots: volume, integer count of lots >= 1.
3264        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3265        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3266                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3267        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3268                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3269        :param expDate: string "Undefined" by default or local date in future.
3270                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3271                        This date is converting to UTC format for server.
3272        :return: JSON with response from broker server.
3273        """
3274        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3275
3276    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3277        """
3278        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3279        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3280        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3281        See also: `Order()` docstring.
3282
3283        :param lots: volume, integer count of lots >= 1.
3284        :param targetPrice: target price > 0. This is open trade price for limit order.
3285        :return: JSON with response from broker server.
3286        """
3287        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3288
3289    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3290        """
3291        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3292        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3293        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3294        target price value then broker opens a limit order. See also: `Order()` docstring.
3295
3296        :param lots: volume, integer count of lots >= 1.
3297        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3298        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3299                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3300        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3301                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3302        :param expDate: string "Undefined" by default or local date in future.
3303                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3304                        This date is converting to UTC format for server.
3305        :return: JSON with response from broker server.
3306        """
3307        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3308
3309    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3310        """
3311        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3312
3313        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3314        :param allOrdersIDs: pre-received lists of all active pending orders.
3315                             This avoids unnecessary downloading data from the server.
3316        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3317        """
3318        if self.accountId is None or not self.accountId:
3319            uLogger.error("Variable `accountId` must be defined for using this method!")
3320            raise Exception("Account ID required")
3321
3322        if orderIDs:
3323            if allOrdersIDs is None or not allOrdersIDs:
3324                rawOrders = self.RequestPendingOrders()
3325                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3326
3327            if allStopOrdersIDs is None or not allStopOrdersIDs:
3328                rawStopOrders = self.RequestStopOrders()
3329                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3330
3331            for orderID in orderIDs:
3332                idInPendingOrders = orderID in allOrdersIDs
3333                idInStopOrders = orderID in allStopOrdersIDs
3334
3335                if not (idInPendingOrders or idInStopOrders):
3336                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3337                    continue
3338
3339                else:
3340                    if idInPendingOrders:
3341                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3342
3343                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3344                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3345                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3346                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3347
3348                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3349                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3350                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3351
3352                        else:
3353                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3354
3355                    elif idInStopOrders:
3356                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3357
3358                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3359                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3360                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3361                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3362
3363                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3364                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3365                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3366
3367                        else:
3368                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3369
3370                    else:
3371                        continue
3372
3373    def CloseAllOrders(self) -> None:
3374        """
3375        Gets a list of open pending and stop orders and cancel it all.
3376        """
3377        rawOrders = self.RequestPendingOrders()
3378        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3379        lenOrders = len(allOrdersIDs)
3380
3381        rawStopOrders = self.RequestStopOrders()
3382        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3383        lenSOrders = len(allStopOrdersIDs)
3384
3385        if lenOrders > 0 or lenSOrders > 0:
3386            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3387
3388            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3389
3390        else:
3391            uLogger.info("Orders not found, nothing to cancel.")
3392
3393    def CloseAll(self, *args) -> None:
3394        """
3395        Close all available (not blocked) opened trades and orders.
3396
3397        Also, you can select one or more keywords case-insensitive:
3398        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3399
3400        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3401        """
3402        overview = self.Overview(show=False)  # get all open trades info
3403
3404        if len(args) == 0:
3405            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3406            self.CloseAllOrders()  # close all pending and stop orders
3407
3408            for iType in TKS_INSTRUMENTS:
3409                if iType != "Currencies":
3410                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3411
3412        else:
3413            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3414            lowerArgs = [x.lower() for x in args]
3415
3416            if "orders" in lowerArgs:
3417                self.CloseAllOrders()  # close all pending and stop orders
3418
3419            for iType in TKS_INSTRUMENTS:
3420                if iType.lower() in lowerArgs and iType != "Currencies":
3421                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3422
3423    @staticmethod
3424    def ParseOrderParameters(operation, **inputParameters):
3425        """
3426        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3427
3428        :param operation: string "Buy" or "Sell".
3429        :param inputParameters: this is dict of strings that looks like this
3430               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3431               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3432               "prices" key: one or more prices to open limit-orders
3433               Counts of values in lots and prices lists must be equals!
3434        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3435        """
3436        # TODO: update order grid work with api v2
3437        pass
3438        # uLogger.debug("Input parameters: {}".format(inputParameters))
3439        #
3440        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3441        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3442        #     raise Exception("Incorrect value")
3443        #
3444        # if "l" in inputParameters.keys():
3445        #     inputParameters["lots"] = inputParameters.pop("l")
3446        #
3447        # if "p" in inputParameters.keys():
3448        #     inputParameters["prices"] = inputParameters.pop("p")
3449        #
3450        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3451        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3452        #     raise Exception("Incorrect value")
3453        #
3454        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3455        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3456        #
3457        # if len(lots) != len(prices):
3458        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3459        #     raise Exception("Incorrect value")
3460        #
3461        # uLogger.debug("Extracted parameters for orders:")
3462        # uLogger.debug("lots = {}".format(lots))
3463        # uLogger.debug("prices = {}".format(prices))
3464        #
3465        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3466        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3467        # uLogger.debug("Order parameters: {}".format(result))
3468        #
3469        # return result
3470
3471    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3472        """
3473        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3474
3475        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3476        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3477        """
3478        result = False
3479        msg = "Instrument not defined!"
3480
3481        if portfolio is None or not portfolio:
3482            portfolio = self.Overview(show=False)
3483
3484        if self.ticker:
3485            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3486            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3487
3488            for iType in TKS_INSTRUMENTS:
3489                for instrument in portfolio["stat"][iType]:
3490                    if instrument["ticker"] == self.ticker:
3491                        result = True
3492                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3493                        break
3494
3495        elif self.figi:
3496            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3497            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3498
3499            for iType in TKS_INSTRUMENTS:
3500                for instrument in portfolio["stat"][iType]:
3501                    if instrument["figi"] == self.figi:
3502                        result = True
3503                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3504                        break
3505
3506        else:
3507            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3508
3509        uLogger.debug(msg)
3510
3511        return result
3512
3513    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3514        """
3515        Returns instrument is in the user's portfolio if it presents there.
3516        Instrument must be defined by `ticker` (highly priority) or `figi`.
3517
3518        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3519        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3520        """
3521        result = None
3522        msg = "Instrument not defined!"
3523
3524        if portfolio is None or not portfolio:
3525            portfolio = self.Overview(show=False)
3526
3527        if self.ticker:
3528            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3529            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3530
3531            for iType in TKS_INSTRUMENTS:
3532                for instrument in portfolio["stat"][iType]:
3533                    if instrument["ticker"] == self.ticker:
3534                        result = instrument
3535                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3536                        break
3537
3538        elif self.figi:
3539            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3540            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3541
3542            for iType in TKS_INSTRUMENTS:
3543                for instrument in portfolio["stat"][iType]:
3544                    if instrument["figi"] == self.figi:
3545                        result = instrument
3546                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3547                        break
3548
3549        else:
3550            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3551
3552        uLogger.debug(msg)
3553
3554        return result
3555
3556    def RequestLimits(self) -> dict:
3557        """
3558        Method for obtaining the available funds for withdrawal for current `accountId`.
3559
3560        See also:
3561        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3562        - `OverviewLimits()` method
3563
3564        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3565                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3566                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3567                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3568        """
3569        if self.accountId is None or not self.accountId:
3570            uLogger.error("Variable `accountId` must be defined for using this method!")
3571            raise Exception("Account ID required")
3572
3573        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3574
3575        self.body = str({"accountId": self.accountId})
3576        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3577        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3578
3579        uLogger.debug("Records about available funds for withdrawal successfully received")
3580
3581        return rawLimits
3582
3583    def OverviewLimits(self, show: bool = False) -> dict:
3584        """
3585        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3586
3587        See also: `RequestLimits()`.
3588
3589        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3590        :return: dict with raw parsed data from server and some calculated statistics about it.
3591        """
3592        if self.accountId is None or not self.accountId:
3593            uLogger.error("Variable `accountId` must be defined for using this method!")
3594            raise Exception("Account ID required")
3595
3596        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3597
3598        view = {
3599            "rawLimits": rawLimits,
3600            "limits": {  # parsed data for every currency:
3601                "money": {  # this is an array of portfolio currency positions
3602                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3603                },
3604                "blocked": {  # this is an array of blocked currency
3605                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3606                },
3607                "blockedGuarantee": {  # this is locked money under collateral for futures
3608                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3609                },
3610            },
3611        }
3612
3613        # --- Prepare text table with limits in human-readable format:
3614        if show:
3615            info = [
3616                "# Withdrawal limits\n\n",
3617                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3618                "* **Account ID:** [{}]\n".format(self.accountId),
3619                "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3620                "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3621            ]
3622
3623            for curr in view["limits"]["money"].keys():
3624                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3625                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3626                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3627
3628                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3629                    "[{}]".format(curr),
3630                    "{:.2f}".format(view["limits"]["money"][curr]),
3631                    "{:.2f}".format(availableMoney),
3632                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3633                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3634                )
3635
3636                if curr == "rub":
3637                    info.insert(5, infoStr)  # insert at first position in table and after headers
3638
3639                else:
3640                    info.append(infoStr)
3641
3642            infoText = "".join(info)
3643
3644            uLogger.info(infoText)
3645
3646            if self.withdrawalLimitsFile:
3647                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3648                    fH.write(infoText)
3649
3650                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3651
3652        return view
3653
3654    def RequestAccounts(self) -> dict:
3655        """
3656        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3657
3658        See also:
3659        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3660        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3661        - `OverviewUserInfo()` method
3662
3663        :return: dict with raw data from server that contains accounts info. Example of dict:
3664                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3665                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3666                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3667                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3668        """
3669        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3670
3671        self.body = str({})
3672        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3673        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3674
3675        uLogger.debug("Records about available accounts successfully received")
3676
3677        return rawAccounts
3678
3679    def RequestUserInfo(self) -> dict:
3680        """
3681        Method for requesting common user's information.
3682
3683        See also:
3684        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3685        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3686        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3687        - `OverviewUserInfo()` method
3688
3689        :return: dict with raw data from server that contains user's information. Example of dict:
3690                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3691                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3692        """
3693        uLogger.debug("Requesting common user's information. Wait, please...")
3694
3695        self.body = str({})
3696        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3697        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3698
3699        uLogger.debug("Records about current user successfully received")
3700
3701        return rawUserInfo
3702
3703    def RequestMarginStatus(self, accountId: str = None) -> dict:
3704        """
3705        Method for requesting margin calculation for defined account ID.
3706
3707        See also:
3708        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3709        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3710        - `OverviewUserInfo()` method
3711
3712        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3713        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3714                 Example of responses:
3715                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3716                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3717                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3718                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3719                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3720                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3721        """
3722        if accountId is None or not accountId:
3723            if self.accountId is None or not self.accountId:
3724                uLogger.error("Variable `accountId` must be defined for using this method!")
3725                raise Exception("Account ID required")
3726
3727            else:
3728                accountId = self.accountId  # use `self.accountId` (main ID) by default
3729
3730        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3731
3732        self.body = str({"accountId": accountId})
3733        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3734        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3735
3736        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3737            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3738            rawMargin = {}
3739
3740        else:
3741            uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3742
3743        return rawMargin
3744
3745    def RequestTariffLimits(self) -> dict:
3746        """
3747        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3748
3749        See also:
3750        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3751        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3752        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3753        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3754        - `OverviewUserInfo()` method
3755
3756        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3757                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3758                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3759        """
3760        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3761
3762        self.body = str({})
3763        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3764        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3765
3766        uLogger.debug("Records with limits of current tariff successfully received")
3767
3768        return rawTariffLimits
3769
3770    def RequestBondCoupons(self, iJSON: dict) -> dict:
3771        """
3772        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3773        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3774        All dates are in UTC timezone.
3775
3776        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3777        Documentation:
3778        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3779        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3780
3781        See also: `ExtendBondsData()`.
3782
3783        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3784                      If raw iJSON is not data of bond then server returns an error [400] with message:
3785                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3786        :return: dictionary with bond payment calendar. Response example
3787                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3788                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3789                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3790                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3791        """
3792        if iJSON["figi"] is None or not iJSON["figi"]:
3793            uLogger.error("FIGI must be defined for using this method!")
3794            raise Exception("FIGI required")
3795
3796        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3797        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3798
3799        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3800            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3801            self.figi,
3802            startDate,
3803            endDate,
3804        ))
3805
3806        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3807        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3808        calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False)
3809
3810        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3811            uLogger.warning("Instrument type is not bond!")
3812
3813        else:
3814            uLogger.debug("Records about bond payment calendar successfully received")
3815
3816        return calendar
3817
3818    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3819        """
3820        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3821        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3822        coupon yields, current yields and some statistics etc.
3823
3824        WARNING! This is too long operation if a lot of bonds requested from broker server.
3825
3826        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3827
3828        :param instruments: list of strings with tickers or FIGIs.
3829        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3830                     for further used by data scientists or stock analytics.
3831        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3832                 In XLSX-file and Pandas DataFrame fields mean:
3833                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3834                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3835        """
3836        if instruments is None or not instruments:
3837            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3838            raise Exception("Ticker or FIGI required")
3839
3840        if isinstance(instruments, str):
3841            instruments = [instruments]
3842
3843        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3844
3845        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3846
3847        iCount = len(uniqueInstruments)
3848        tooLong = iCount >= 20
3849        if tooLong:
3850            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3851
3852        bonds = None
3853        for i, self.figi in enumerate(uniqueInstruments):
3854            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3855
3856            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3857                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3858                rawBond = self.SearchByFIGI(requestPrice=True)
3859
3860                # Widen raw data with UTC current time (iData["actualDateTime"]):
3861                actualDate = datetime.now(tzutc())
3862                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3863
3864                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3865                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3866
3867                # Replace some values with human-readable:
3868                iData["nominalCurrency"] = iData["nominal"]["currency"]
3869                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3870                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3871                iData["aciCurrency"] = iData["aciValue"]["currency"]
3872                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3873                iData["issueSize"] = int(iData["issueSize"])
3874                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3875                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3876                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3877                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3878                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3879                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3880                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3881                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3882                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3883                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3884
3885                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3886                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3887                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3888                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3889                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3890                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3891                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3892                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3893                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3894                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3895                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3896
3897                # Widen raw data with calendar data from `rawCalendar` values:
3898                calendarData = []
3899                for item in iData["rawCalendar"]["events"]:
3900                    calendarData.append({
3901                        "couponDate": item["couponDate"],
3902                        "couponNumber": int(item["couponNumber"]),
3903                        "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3904                        "payCurrency": item["payOneBond"]["currency"],
3905                        "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3906                        "couponType": TKS_COUPON_TYPES[item["couponType"]],
3907                        "couponStartDate": item["couponStartDate"],
3908                        "couponEndDate": item["couponEndDate"],
3909                        "couponPeriod": item["couponPeriod"],
3910                    })
3911
3912                # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3913                if "maturityDate" not in iData.keys():
3914                    iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3915
3916                # Widen raw data with Coupon Rate.
3917                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3918                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3919                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3920                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3921
3922                # Widen raw data with Yield to Maturity (YTM) on current date.
3923                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3924                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3925                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3926                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3927                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3928                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3929
3930                iData["calendar"] = calendarData  # adds calendar at the end
3931
3932                # Remove not used data:
3933                iData.pop("uid")
3934                iData.pop("positionUid")
3935                iData.pop("currentPrice")
3936                iData.pop("rawCalendar")
3937
3938                colNames = list(iData.keys())
3939                if bonds is None:
3940                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3941
3942                else:
3943                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3944
3945            else:
3946                uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"]))
3947
3948            processed = round(100 * (i + 1) / iCount, 1)
3949            if tooLong and processed % 5 == 0:
3950                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3951
3952            else:
3953                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3954
3955        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3956
3957        # Saving bonds from Pandas DataFrame to XLSX sheet:
3958        if xlsx and self.bondsXLSXFile:
3959            with pd.ExcelWriter(
3960                    path=self.bondsXLSXFile,
3961                    date_format=TKS_DATE_FORMAT,
3962                    datetime_format=TKS_DATE_TIME_FORMAT,
3963                    mode="w",
3964            ) as writer:
3965                bonds.to_excel(
3966                    writer,
3967                    sheet_name="Extended bonds data",
3968                    index=True,
3969                    encoding="UTF-8",
3970                    freeze_panes=(1, 1),
3971                )  # saving as XLSX-file with freeze first row and column as headers
3972
3973            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3974
3975        return bonds
3976
3977    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3978        """
3979        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
3980
3981        WARNING! This is too long operation if a lot of bonds requested from broker server.
3982
3983        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3984
3985        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
3986                        extended information about bonds: main info, current prices, bond payment calendar,
3987                        coupon yields, current yields and some statistics etc.
3988                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3989        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
3990                     for further used by data scientists or stock analytics.
3991        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
3992        """
3993        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3994            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3995
3996        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3997
3998        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3999        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4000        calendar = None
4001        for bond in extBonds.iterrows():
4002            for item in bond[1]["calendar"]:
4003                cData = {
4004                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4005                    "couponDate": item["couponDate"],
4006                    "figi": bond[1]["figi"],
4007                    "ticker": bond[1]["ticker"],
4008                    "name": bond[1]["name"],
4009                    "couponNumber": item["couponNumber"],
4010                    "payOneBond": item["payOneBond"],
4011                    "payCurrency": item["payCurrency"],
4012                    "couponType": item["couponType"],
4013                    "couponPeriod": item["couponPeriod"],
4014                    "fixDate": item["fixDate"],
4015                    "couponStartDate": item["couponStartDate"],
4016                    "couponEndDate": item["couponEndDate"],
4017                }
4018
4019                if calendar is None:
4020                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4021
4022                else:
4023                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4024
4025        calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4026
4027        # Saving calendar from Pandas DataFrame to XLSX sheet:
4028        if xlsx:
4029            xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4030
4031            with pd.ExcelWriter(
4032                    path=xlsxCalendarFile,
4033                    date_format=TKS_DATE_FORMAT,
4034                    datetime_format=TKS_DATE_TIME_FORMAT,
4035                    mode="w",
4036            ) as writer:
4037                humanReadable = calendar.copy(deep=True)
4038                humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4039                humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4040                humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4041                humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4042                humanReadable.columns = colNames  # human-readable column names
4043
4044                humanReadable.to_excel(
4045                    writer,
4046                    sheet_name="Bond payments calendar",
4047                    index=False,
4048                    encoding="UTF-8",
4049                    freeze_panes=(1, 2),
4050                )  # saving as XLSX-file with freeze first row and column as headers
4051
4052                del humanReadable  # release df in memory
4053
4054            uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4055
4056        return calendar
4057
4058    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4059        """
4060        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4061        Also, creates Markdown file with calendar data, `calendar.md` by default.
4062
4063        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4064
4065        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4066                        extended information about bonds: main info, current prices, bond payment calendar,
4067                        coupon yields, current yields and some statistics etc.
4068                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4069        :param show: if `True` then also printing bonds payment calendar to the console,
4070                     otherwise save to file `calendarFile` only. `False` by default.
4071        :return: multilines text in Markdown format with bonds payment calendar as a table.
4072        """
4073        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4074            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4075
4076        infoText = "# Bond payments calendar\n\n"
4077
4078        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4079
4080        if not calendar.empty:
4081            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4082
4083            info = [
4084                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4085                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4086            ]
4087
4088            newMonth = False
4089            notOneBond = calendar["figi"].nunique() > 1
4090            for i, bond in enumerate(calendar.iterrows()):
4091                if newMonth and notOneBond:
4092                    info.append(splitLine)
4093
4094                info.append(
4095                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4096                        "  √" if bond[1]["paid"] else "  —",
4097                        bond[1]["couponDate"].split("T")[0],
4098                        bond[1]["figi"],
4099                        bond[1]["ticker"],
4100                        bond[1]["couponNumber"],
4101                        "{} {}".format(
4102                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4103                            bond[1]["payCurrency"],
4104                        ),
4105                        bond[1]["couponType"],
4106                        bond[1]["couponPeriod"],
4107                        bond[1]["fixDate"].split("T")[0],
4108                    )
4109                )
4110
4111                if i < len(calendar.values) - 1:
4112                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4113                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4114                    newMonth = False if curDate.month == nextDate.month else True
4115
4116                else:
4117                    newMonth = False
4118
4119            infoText += "".join(info)
4120
4121            if show:
4122                uLogger.info("{}".format(infoText))
4123
4124            if self.calendarFile is not None:
4125                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4126                    fH.write(infoText)
4127
4128                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4129
4130        else:
4131            infoText += "No data\n"
4132
4133        return infoText
4134
4135    def OverviewAccounts(self, show: bool = False) -> dict:
4136        """
4137        Method for parsing and show simple table with all available user accounts.
4138
4139        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4140
4141        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4142        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4143                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4144                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4145                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4146                                                        "closed": "—", "access": "Full access" }, ...}}`
4147        """
4148        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4149
4150        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4151        accounts = {
4152            item["id"]: {
4153                "type": TKS_ACCOUNT_TYPES[item["type"]],
4154                "name": item["name"],
4155                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4156                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4157                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4158                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4159            } for item in rawAccounts["accounts"]
4160        }
4161
4162        # Raw and parsed data with some fields replaced in "stat" section:
4163        view = {
4164            "rawAccounts": rawAccounts,
4165            "stat": accounts,
4166        }
4167
4168        # --- Prepare simple text table with only accounts data in human-readable format:
4169        if show:
4170            info = [
4171                "# User accounts\n\n",
4172                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4173                "| Account ID   | Type                      | Status                    | Name                           |\n",
4174                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4175            ]
4176
4177            for account in view["stat"].keys():
4178                info.extend([
4179                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4180                        account,
4181                        view["stat"][account]["type"],
4182                        view["stat"][account]["status"],
4183                        view["stat"][account]["name"],
4184                    )
4185                ])
4186
4187            infoText = "".join(info)
4188
4189            uLogger.info(infoText)
4190
4191            if self.userAccountsFile:
4192                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4193                    fH.write(infoText)
4194
4195                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4196
4197        return view
4198
4199    def OverviewUserInfo(self, show: bool = False) -> dict:
4200        """
4201        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4202
4203        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4204
4205        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4206        :return: dict with raw parsed data from server and some calculated statistics about it.
4207        """
4208        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4209        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4210        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4211        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4212        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4213        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4214
4215        # This is dict with parsed common user data:
4216        userInfo = {
4217            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4218            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4219            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4220            "tariff": rawUserInfo["tariff"],
4221        }
4222
4223        # This is an array of dict with parsed margin statuses for every account IDs:
4224        margins = {}
4225        for accountId in accounts.keys():
4226            if rawMargins[accountId]:
4227                margins[accountId] = {
4228                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4229                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4230                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4231                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4232                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4233                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4234                }
4235
4236            else:
4237                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4238
4239        unary = {}  # unary-connection limits
4240        for item in rawTariffLimits["unaryLimits"]:
4241            if item["limitPerMinute"] in unary.keys():
4242                unary[item["limitPerMinute"]].extend(item["methods"])
4243
4244            else:
4245                unary[item["limitPerMinute"]] = item["methods"]
4246
4247        stream = {}  # stream-connection limits
4248        for item in rawTariffLimits["streamLimits"]:
4249            if item["limit"] in stream.keys():
4250                stream[item["limit"]].extend(item["streams"])
4251
4252            else:
4253                stream[item["limit"]] = item["streams"]
4254
4255        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4256        limits = {
4257            "unary": unary,
4258            "stream": stream,
4259        }
4260
4261        # Raw and parsed data as an output result:
4262        view = {
4263            "rawUserInfo": rawUserInfo,
4264            "rawAccounts": rawAccounts,
4265            "rawMargins": rawMargins,
4266            "rawTariffLimits": rawTariffLimits,
4267            "stat": {
4268                "userInfo": userInfo,
4269                "accounts": accounts,
4270                "margins": margins,
4271                "limits": limits,
4272            },
4273        }
4274
4275        # --- Prepare text table with user information in human-readable format:
4276        if show:
4277            info = [
4278                "# Full user information\n\n",
4279                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4280                "## Common information\n\n",
4281                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4282                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4283                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4284                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4285                "\n## User accounts\n\n",
4286            ]
4287
4288            for account in view["stat"]["accounts"].keys():
4289                info.extend([
4290                    "### ID: [{}]\n\n".format(account),
4291                    "| Parameters           | Values                                                       |\n",
4292                    "|----------------------|--------------------------------------------------------------|\n",
4293                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4294                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4295                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4296                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4297                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4298                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4299                ])
4300
4301                if margins[account]:
4302                    info.extend([
4303                        "| Margin status:       | Enabled                                                      |\n",
4304                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4305                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4306                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4307                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4308                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4309                    ])
4310
4311                else:
4312                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4313
4314            info.extend([
4315                "\n## Current user tariff limits\n",
4316                "\nSee also:\n",
4317                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4318                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4319                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4320                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4321                "\n### Unary limits\n",
4322            ])
4323
4324            if unary:
4325                for key, values in sorted(unary.items()):
4326                    info.append("\n* Max requests per minute: {}\n".format(key))
4327
4328                    for value in values:
4329                        info.append("  - {}\n".format(value))
4330
4331            else:
4332                info.append("\nNot available\n")
4333
4334            info.append("\n### Stream limits\n")
4335
4336            if stream:
4337                for key, values in sorted(stream.items()):
4338                    info.append("\n* Max stream connections: {}\n".format(key))
4339
4340                    for value in values:
4341                        info.append("  - {}\n".format(value))
4342
4343            else:
4344                info.append("\nNot available\n")
4345
4346            infoText = "".join(info)
4347
4348            uLogger.info(infoText)
4349
4350            if self.userInfoFile:
4351                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4352                    fH.write(infoText)
4353
4354                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4355
4356        return view
4357
4358
4359class Args:
4360    """
4361    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4362    """
4363    def __init__(self, **kwargs):
4364        self.__dict__.update(kwargs)
4365
4366    def __getattr__(self, item):
4367        return None
4368
4369
4370def ParseArgs():
4371    """This function get and parse command line keys."""
4372    parser = ArgumentParser()  # command-line string parser
4373
4374    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4375    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4376
4377    # --- options:
4378
4379    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4380    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4381    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4382
4383    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4384    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4385
4386    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4387    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4388
4389    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4390
4391    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4392    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4393    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4394
4395    parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4396
4397    # --- commands:
4398
4399    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4400
4401    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4402    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4403    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4404    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4405    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4406    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4407    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4408    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4409
4410    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4411    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4412    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4413    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4414    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4415
4416    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4417    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4418    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4419    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4420
4421    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4422    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4423    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4424
4425    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4426    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4427    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4428    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4429    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4430    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4431    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4432
4433    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4434    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4435    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.")
4436    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.")
4437    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4438
4439    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4440    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4441    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4442
4443    cmdArgs = parser.parse_args()
4444    return cmdArgs
4445
4446
4447def Main(**kwargs):
4448    """
4449    Main function for work with TKSBrokerAPI in the console.
4450
4451    See examples:
4452    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4453    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4454    """
4455    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4456
4457    if args.debug_level:
4458        uLogger.level = 10  # always debug level by default
4459        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4460
4461    exitCode = 0
4462    start = datetime.now(tzutc())
4463    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4464        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4465        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4466    ))
4467
4468    # trying to calculate full current version:
4469    buildVersion = __version__
4470    try:
4471        v = version("tksbrokerapi")
4472        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4473
4474    except Exception:
4475        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4476
4477    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4478    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4479
4480    try:
4481        if args.version:
4482            print("TKSBrokerAPI {}".format(buildVersion))
4483            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4484
4485        else:
4486            # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader`
4487            server = TinkoffBrokerServer(
4488                token=args.token,
4489                accountId=args.account_id,
4490                useCache=not args.no_cache,
4491            )
4492
4493            # --- set some options:
4494
4495            if args.ticker:
4496                if args.ticker in server.aliasesKeys:
4497                    server.ticker = server.aliases[args.ticker]  # Replace some tickers with its aliases
4498
4499                else:
4500                    server.ticker = args.ticker
4501
4502            if args.figi:
4503                server.figi = args.figi
4504
4505            if args.depth is not None:
4506                server.depth = args.depth
4507
4508            # --- do one of commands:
4509
4510            if args.list:
4511                if args.output is not None:
4512                    server.instrumentsFile = args.output
4513
4514                server.ShowInstrumentsInfo(show=True)
4515
4516            elif args.list_xlsx:
4517                server.DumpInstrumentsAsXLSX(forceUpdate=False)
4518
4519            elif args.bonds_xlsx is not None:
4520                if args.output is not None:
4521                    server.bondsXLSXFile = args.output
4522
4523                if len(args.bonds_xlsx) == 0:
4524                    server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4525
4526                else:
4527                    server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4528
4529            elif args.search:
4530                if args.output is not None:
4531                    server.searchResultsFile = args.output
4532
4533                server.SearchInstruments(pattern=args.search[0], show=True)
4534
4535            elif args.info:
4536                if not (args.ticker or args.figi):
4537                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4538                    raise Exception("Ticker or FIGI required")
4539
4540                if args.output is not None:
4541                    server.infoFile = args.output
4542
4543                if args.ticker:
4544                    server.SearchByTicker(requestPrice=True, show=True, debug=False)  # show info and current prices by ticker name
4545
4546                else:
4547                    server.SearchByFIGI(requestPrice=True, show=True, debug=False)  # show info and current prices by FIGI id
4548
4549            elif args.calendar is not None:
4550                if args.output is not None:
4551                    server.calendarFile = args.output
4552
4553                if len(args.calendar) == 0:
4554                    bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4555
4556                else:
4557                    bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4558
4559                server.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4560
4561            elif args.price:
4562                if not (args.ticker or args.figi):
4563                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4564                    raise Exception("Ticker or FIGI required")
4565
4566                server.GetCurrentPrices(show=True)
4567
4568            elif args.prices is not None:
4569                if args.output is not None:
4570                    server.pricesFile = args.output
4571
4572                server.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4573
4574            elif args.overview:
4575                if args.output is not None:
4576                    server.overviewFile = args.output
4577
4578                server.Overview(show=True, details="full")
4579
4580            elif args.overview_digest:
4581                if args.output is not None:
4582                    server.overviewDigestFile = args.output
4583
4584                server.Overview(show=True, details="digest")
4585
4586            elif args.overview_positions:
4587                if args.output is not None:
4588                    server.overviewPositionsFile = args.output
4589
4590                server.Overview(show=True, details="positions")
4591
4592            elif args.overview_orders:
4593                if args.output is not None:
4594                    server.overviewOrdersFile = args.output
4595
4596                server.Overview(show=True, details="orders")
4597
4598            elif args.overview_analytics:
4599                if args.output is not None:
4600                    server.overviewAnalyticsFile = args.output
4601
4602                server.Overview(show=True, details="analytics")
4603
4604            elif args.deals is not None:
4605                if args.output is not None:
4606                    server.reportFile = args.output
4607
4608                if 0 <= len(args.deals) < 3:
4609                    server.Deals(
4610                        start=args.deals[0] if len(args.deals) >= 1 else None,
4611                        end=args.deals[1] if len(args.deals) == 2 else None,
4612                        show=True,  # Always show deals report in console
4613                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4614                    )
4615
4616                else:
4617                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4618                    raise Exception("Incorrect value")
4619
4620            elif args.history is not None:
4621                if args.output is not None:
4622                    server.historyFile = args.output
4623
4624                if 0 <= len(args.history) < 3:
4625                    dataReceived = server.History(
4626                        start=args.history[0] if len(args.history) >= 1 else None,
4627                        end=args.history[1] if len(args.history) == 2 else None,
4628                        interval="hour" if args.interval is None or not args.interval else args.interval,
4629                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4630                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4631                        show=True,  # shows all downloaded candles in console
4632                    )
4633
4634                    if args.render_chart is not None and dataReceived is not None:
4635                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4636
4637                        server.ShowHistoryChart(
4638                            candles=dataReceived,
4639                            interact=iChart,
4640                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4641                        )
4642
4643                else:
4644                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4645                    raise Exception("Incorrect value")
4646
4647            elif args.load_history is not None:
4648                histData = server.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4649
4650                if args.render_chart is not None and histData is not None:
4651                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4652                    server.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4653
4654                    server.ShowHistoryChart(
4655                        candles=histData,
4656                        interact=iChart,
4657                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4658                    )
4659
4660            elif args.trade is not None:
4661                if 1 <= len(args.trade) <= 5:
4662                    server.Trade(
4663                        operation=args.trade[0],
4664                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4665                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4666                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4667                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4668                    )
4669
4670                else:
4671                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4672
4673            elif args.buy is not None:
4674                if 0 <= len(args.buy) <= 4:
4675                    server.Buy(
4676                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4677                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4678                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4679                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4680                    )
4681
4682                else:
4683                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4684
4685            elif args.sell is not None:
4686                if 0 <= len(args.sell) <= 4:
4687                    server.Sell(
4688                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4689                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4690                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4691                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4692                    )
4693
4694                else:
4695                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4696
4697            elif args.order:
4698                if 4 <= len(args.order) <= 7:
4699                    server.Order(
4700                        operation=args.order[0],
4701                        orderType=args.order[1],
4702                        lots=int(args.order[2]),
4703                        targetPrice=float(args.order[3]),
4704                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4705                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4706                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4707                    )
4708
4709                else:
4710                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4711
4712            elif args.buy_limit:
4713                server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4714
4715            elif args.sell_limit:
4716                server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4717
4718            elif args.buy_stop:
4719                if 2 <= len(args.buy_stop) <= 7:
4720                    server.BuyStop(
4721                        lots=int(args.buy_stop[0]),
4722                        targetPrice=float(args.buy_stop[1]),
4723                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4724                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4725                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4726                    )
4727
4728                else:
4729                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4730
4731            elif args.sell_stop:
4732                if 2 <= len(args.sell_stop) <= 7:
4733                    server.SellStop(
4734                        lots=int(args.sell_stop[0]),
4735                        targetPrice=float(args.sell_stop[1]),
4736                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4737                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4738                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4739                    )
4740
4741                else:
4742                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4743
4744            # elif args.buy_order_grid is not None:
4745            #     # update order grid work with api v2
4746            #     if len(args.buy_order_grid) == 2:
4747            #         orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4748            #
4749            #         for order in orderParams:
4750            #             server.Order(operation="Buy", lots=order["lot"], price=order["price"])
4751            #
4752            #     else:
4753            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4754            #
4755            # elif args.sell_order_grid is not None:
4756            #     # update order grid work with api v2
4757            #     if len(args.sell_order_grid) >= 2:
4758            #         orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4759            #
4760            #         for order in orderParams:
4761            #             server.Order(operation="Sell", lots=order["lot"], price=order["price"])
4762            #
4763            #     else:
4764            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4765
4766            elif args.close_order is not None:
4767                server.CloseOrders(args.close_order)  # close only one order
4768
4769            elif args.close_orders is not None:
4770                server.CloseOrders(args.close_orders)  # close list of orders
4771
4772            elif args.close_trade:
4773                if not args.ticker:
4774                    uLogger.error("`--ticker` key is required for this operation!")
4775                    raise Exception("Ticker required")
4776
4777                server.CloseTrades([args.ticker])  # close only one trade
4778
4779            elif args.close_trades is not None:
4780                server.CloseTrades(args.close_trades)  # close trades for list of tickers
4781
4782            elif args.close_all is not None:
4783                server.CloseAll(*args.close_all)
4784
4785            elif args.limits:
4786                if args.output is not None:
4787                    server.withdrawalLimitsFile = args.output
4788
4789                server.OverviewLimits(show=True)
4790
4791            elif args.user_info:
4792                if args.output is not None:
4793                    server.userInfoFile = args.output
4794
4795                server.OverviewUserInfo(show=True)
4796
4797            elif args.account:
4798                if args.output is not None:
4799                    server.userAccountsFile = args.output
4800
4801                server.OverviewAccounts(show=True)
4802
4803            else:
4804                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4805                raise Exception("There is no command to execute")
4806
4807    except Exception:
4808        trace = tb.format_exc()
4809        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4810            if e in trace:
4811                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4812                break
4813
4814        uLogger.debug(trace)
4815        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4816        exitCode = 255  # an error occurred, must be open a ticket for this issue
4817
4818    finally:
4819        finish = datetime.now(tzutc())
4820
4821        if exitCode == 0:
4822            uLogger.debug("All operations were finished success (summary code is 0).")
4823
4824        else:
4825            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4826                os.path.abspath(uLog.defaultLogFile), exitCode,
4827            ))
4828
4829        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4830        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4831            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4832            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4833        ))
4834
4835        if not kwargs:
4836            sys.exit(exitCode)
4837
4838        else:
4839            return exitCode
4840
4841
4842if __name__ == "__main__":
4843    Main()
def NanoToFloat(units: str, nano: int) -> float:
80def NanoToFloat(units: str, nano: int) -> float:
81    """
82    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
83
84    `NanoToFloat(units="2", nano=500000000) -> 2.5`
85
86    `NanoToFloat(units="0", nano=50000000) -> 0.05`
87
88    :param units: integer string or integer parameter that represents the integer part of number
89    :param nano: integer string or integer parameter that represents the fractional part of number
90    :return: float view of number
91    """
92    return int(units) + int(nano) * NANO

Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:

NanoToFloat(units="2", nano=500000000) -> 2.5

NanoToFloat(units="0", nano=50000000) -> 0.05

Parameters
  • units: integer string or integer parameter that represents the integer part of number
  • nano: integer string or integer parameter that represents the fractional part of number
Returns

float view of number

def FloatToNano(number: float) -> dict:
 95def FloatToNano(number: float) -> dict:
 96    """
 97    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
 98
 99    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
100
101    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
102
103    :param number: float number
104    :return: nano-type view of number: `{"units": "string", "nano": integer}`
105    """
106    splitByPoint = str(number).split(".")
107    frac = 0
108
109    if len(splitByPoint) > 1:
110        if len(splitByPoint[1]) <= 9:
111            frac = int("{}{}".format(
112                int(splitByPoint[1]),
113                "0" * (9 - len(splitByPoint[1])),
114            ))
115
116    if (number < 0) and (frac > 0):
117        frac = -frac
118
119    return {"units": str(int(number)), "nano": frac}

Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:

FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}

FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}

Parameters
  • number: float number
Returns

nano-type view of number: {"units": "string", "nano": integer}

def GetDatesAsString(start: str = None, end: str = None) -> tuple:
122def GetDatesAsString(start: str = None, end: str = None) -> tuple:
123    """
124    Create tuple of date and time strings with timezone parsed from user-friendly date.
125
126    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
127
128    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
129    An error exception will occur if input date has incorrect format.
130
131    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
132    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
133    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
134    Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago.
135
136    Also, you can use keywords for start if `end=None`:
137    `today` (from 00:00:00 to the end of current day),
138    `yesterday` (-1 day from 00:00:00 to 23:59:59),
139    `week` (-7 day from 00:00:00 to the end of current day),
140    `month` (-30 day from 00:00:00 to the end of current day),
141    `year` (-365 day from 00:00:00 to the end of current day),
142
143    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
144             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
145             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
146    """
147    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
148    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
149    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
150
151    # time between start and the end of the current day:
152    if start is None or start.lower() == "today":
153        pass
154
155    # from start of the last day to the end of the last day:
156    elif start.lower() == "yesterday":
157        s -= timedelta(days=1)
158        e -= timedelta(days=1)
159
160    # week (-7 day from 00:00:00 to the end of the current day):
161    elif start.lower() == "week":
162        s -= timedelta(days=6)  # +1 current day already taken into account
163
164    # month (-30 day from 00:00:00 to the end of current day):
165    elif start.lower() == "month":
166        s -= timedelta(days=29)  # +1 current day already taken into account
167
168    # year (-365 day from 00:00:00 to the end of current day):
169    elif start.lower() == "year":
170        s -= timedelta(days=364)  # +1 current day already taken into account
171
172    # -N days ago to the end of current day:
173    elif start.startswith('-') and start[1:].isdigit():
174        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
175
176    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
177    else:
178        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
179        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
180
181    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
182    s = s.strftime(TKS_DATE_TIME_FORMAT)
183    e = e.strftime(TKS_DATE_TIME_FORMAT)
184
185    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
186
187    return s, e

Create tuple of date and time strings with timezone parsed from user-friendly date.

User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).

Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.

If start=None, end=None then return dates from yesterday to the end of the day. If start=some_date_1, end=None then return dates from some_date_1 to the end of the day. If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2. Start day may be negative integer numbers: -1, -2, -3 - how many days ago.

Also, you can use keywords for start if end=None: today (from 00:00:00 to the end of current day), yesterday (-1 day from 00:00:00 to 23:59:59), week (-7 day from 00:00:00 to the end of current day), month (-30 day from 00:00:00 to the end of current day), year (-365 day from 00:00:00 to the end of current day),

Returns

tuple with 2 strings (start, end) dates in UTC ISO time format %Y-%m-%dT%H:%M:%SZ for OpenAPI. See date and time format here: TKSEnums.TKS_DATE_TIME_FORMAT. Example: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.

class TinkoffBrokerServer:
 190class TinkoffBrokerServer:
 191    """
 192    This class implements methods to work with Tinkoff broker server.
 193
 194    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 195
 196    About `token`: https://tinkoff.github.io/investAPI/token/
 197    """
 198    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 199        """
 200        Main class init.
 201
 202        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 203        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 204                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 205        :param useCache: use default cache file with raw data to use instead of `iList`.
 206                         True by default. Cache is auto-update if new day has come.
 207                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 208        :param defaultCache: path to default cache file. `dump.json` by default.
 209        """
 210        if token is None or not token:
 211            try:
 212                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 213                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 214
 215            except KeyError:
 216                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 217                raise Exception("Token required")
 218
 219        else:
 220            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 221            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 222
 223        if accountId is None or not accountId:
 224            try:
 225                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 226                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 227
 228            except KeyError:
 229                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 230
 231        else:
 232            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 233            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 234
 235        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 236        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 237
 238        Latest version: https://pypi.org/project/tksbrokerapi/
 239        """
 240
 241        self.aliases = TKS_TICKER_ALIASES
 242        """Some aliases instead official tickers.
 243
 244        See also: `TKSEnums.TKS_TICKER_ALIASES`
 245        """
 246
 247        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 248
 249        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 250
 251        self.ticker = ""
 252        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 253
 254        See also: `SearchByTicker()`, `SearchInstruments()`.
 255        """
 256
 257        self.figi = ""
 258        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 259
 260        See also: `SearchByFIGI()`, `SearchInstruments()`.
 261        """
 262
 263        self.depth = 1
 264        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 265
 266        See also: `GetCurrentPrices()`.
 267        """
 268
 269        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 270        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 271
 272        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 273        """
 274
 275        uLogger.debug("Broker API server: {}".format(self.server))
 276
 277        self.timeout = 15
 278        """Server operations timeout in seconds. Default: `15`.
 279
 280        See also: `SendAPIRequest()`.
 281        """
 282
 283        self.headers = {
 284            "Content-Type": "application/json",
 285            "accept": "application/json",
 286            "Authorization": "Bearer {}".format(self.token),
 287            "x-app-name": "Tim55667757.TKSBrokerAPI",
 288        }
 289        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 290
 291        See also: `SendAPIRequest()`.
 292        """
 293
 294        self.body = None
 295        """Request body which send to broker server. Default: `None`.
 296
 297        See also: `SendAPIRequest()`.
 298        """
 299
 300        self.historyFile = None
 301        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 302
 303        See also: `History()`.
 304        """
 305
 306        self.htmlHistoryFile = "index.html"
 307        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 308
 309        See also: `ShowHistoryChart()`.
 310        """
 311
 312        self.instrumentsFile = "instruments.md"
 313        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 314
 315        See also: `ShowInstrumentsInfo()`.
 316        """
 317
 318        self.searchResultsFile = "search-results.md"
 319        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 320
 321        See also: `SearchInstruments()`.
 322        """
 323
 324        self.pricesFile = "prices.md"
 325        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 326
 327        See also: `GetListOfPrices()`.
 328        """
 329
 330        self.infoFile = "info.md"
 331        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 332
 333        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 334        """
 335
 336        self.bondsXLSXFile = "ext-bonds.xlsx"
 337        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 338        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 339
 340        See also: `ExtendBondsData()`.
 341        """
 342
 343        self.calendarFile = "calendar.md"
 344        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 345        
 346        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 347
 348        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 349        """
 350
 351        self.overviewFile = "overview.md"
 352        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 353
 354        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 355        """
 356
 357        self.overviewDigestFile = "overview-digest.md"
 358        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 359
 360        See also: `Overview()` with parameter `details="digest"`.
 361        """
 362
 363        self.overviewPositionsFile = "overview-positions.md"
 364        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 365
 366        See also: `Overview()` with parameter `details="positions"`.
 367        """
 368
 369        self.overviewOrdersFile = "overview-orders.md"
 370        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 371
 372        See also: `Overview()` with parameter `details="orders"`.
 373        """
 374
 375        self.overviewAnalyticsFile = "overview-analytics.md"
 376        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 377
 378        See also: `Overview()` with parameter `details="analytics"`.
 379        """
 380
 381        self.reportFile = "deals.md"
 382        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 383
 384        See also: `Deals()`.
 385        """
 386
 387        self.withdrawalLimitsFile = "limits.md"
 388        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 389
 390        See also: `OverviewLimits()` and `RequestLimits()`.
 391        """
 392
 393        self.userInfoFile = "user-info.md"
 394        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 395
 396        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 397        """
 398
 399        self.userAccountsFile = "accounts.md"
 400        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 401
 402        See also: `OverviewAccounts()`, `RequestAccounts()`.
 403        """
 404
 405        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 406        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 407
 408        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 409
 410        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 411        """
 412
 413        self.iList = None  # init iList for raw instruments data
 414        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 415        
 416        See also: `Listing()`, `DumpInstruments()`.
 417        """
 418
 419        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 420        if useCache:
 421            if os.path.exists(self.iListDumpFile):
 422                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 423                curTime = datetime.now(tzutc())
 424
 425                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 426                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 427
 428                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 429
 430                else:
 431                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 432
 433                    uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile)))
 434                    uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 435
 436            else:
 437                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 438                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 439
 440        else:
 441            self.iList = self.Listing()  # request new raw instruments data from broker server
 442            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 443
 444        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 445        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 446
 447        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 448        """
 449
 450    @staticmethod
 451    def _ParseJSON(rawData="{}", debug: bool = False) -> dict:
 452        """
 453        Parse JSON from response string.
 454
 455        :param rawData: this is a string with JSON-formatted text.
 456        :param debug: if `True` then print more debug information.
 457        :return: JSON (dictionary), parsed from server response string.
 458        """
 459        if debug:
 460            uLogger.debug("Raw text body:")
 461            uLogger.debug(rawData)
 462
 463        responseJSON = json.loads(rawData) if rawData else {}
 464
 465        if debug:
 466            uLogger.debug("JSON formatted:")
 467            for jsonLine in json.dumps(responseJSON, indent=4).split('\n'):
 468                uLogger.debug(jsonLine)
 469
 470        return responseJSON
 471
 472    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
 473        """
 474        Send GET or POST request to broker server and receive JSON object.
 475
 476        self.header: must be defining with dictionary of headers.
 477        self.body: if define then used as request body. None by default.
 478        self.timeout: global request timeout, 15 seconds by default.
 479        :param url: url with REST request.
 480        :param reqType: send "GET" or "POST" request. "GET" by default.
 481        :param retry: how many times retry after first request if an 5xx server errors occurred.
 482        :param pause: sleep time in seconds between retries.
 483        :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc.
 484        :return: response JSON (dictionary) from broker.
 485        """
 486        if reqType not in ("GET", "POST"):
 487            uLogger.error("You can define request type: 'GET' or 'POST'!")
 488            raise Exception("Incorrect value")
 489
 490        if debug:
 491            uLogger.debug("Request parameters:")
 492            uLogger.debug("    - REST API URL: {}".format(url))
 493            uLogger.debug("    - request type: {}".format(reqType))
 494            uLogger.debug("    - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***")))
 495            uLogger.debug("    - body: {}".format(self.body))
 496
 497        # fast hack to avoid all operations with some tickers/FIGI
 498        responseJSON = {}
 499        oK = True
 500        for item in self.exclude:
 501            if item in url:
 502                if debug:
 503                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 504
 505                oK = False
 506                break
 507
 508        if oK:
 509            counter = 0
 510            response = None
 511            errMsg = ""
 512
 513            while not response and counter <= retry:
 514                if reqType == "GET":
 515                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 516
 517                if reqType == "POST":
 518                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 519
 520                if debug:
 521                    uLogger.debug("Response:")
 522                    uLogger.debug("    - status code: {}".format(response.status_code))
 523                    uLogger.debug("    - reason: {}".format(response.reason))
 524                    uLogger.debug("    - body length: {}".format(len(response.text)))
 525                    uLogger.debug("    - headers: {}".format(response.headers))
 526
 527                # Server returns some headers:
 528                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
 529                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
 530                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
 531                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 532                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 533                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 534                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 535                    sleep(rateLimitWait)
 536
 537                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 538                if 400 <= response.status_code < 500:
 539                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 540                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 541                    counter = retry + 1
 542
 543                if 500 <= response.status_code < 600:
 544                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 545                    uLogger.debug("    - not oK, {}".format(errMsg))
 546                    counter += 1
 547
 548                    if counter <= retry:
 549                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 550                        sleep(pause)
 551
 552            responseJSON = self._ParseJSON(response.text)
 553
 554            if errMsg:
 555                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 556                uLogger.error("    - not oK, {}".format(errMsg))
 557
 558        return responseJSON
 559
 560    def _IUpdater(self, iType: str) -> tuple:
 561        """
 562        Request instrument by type from server. See available API methods for instruments:
 563        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 564        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 565        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 566        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 567        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 568
 569        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 570        :return: tuple with iType name and list of available instruments of current type for defined user token.
 571        """
 572        result = []
 573
 574        if iType in TKS_INSTRUMENTS:
 575            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 576
 577            # all instruments have the same body in API v2 requests:
 578            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 579            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 580            result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"]
 581
 582        return iType, result
 583
 584    def _IWrapper(self, kwargs):
 585        """
 586        Wrapper runs instrument's update method `_IUpdater()`.
 587        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 588        """
 589        return self._IUpdater(**kwargs)
 590
 591    def Listing(self) -> dict:
 592        """
 593        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 594
 595        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 596        """
 597        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 598        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 599
 600        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 601        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 602        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 603
 604        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 605        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 606        poolUpdater.close()
 607
 608        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 609        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 610        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 611
 612        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 613        for iType in iList.keys():
 614            for ticker in iList[iType]:
 615                iList[iType][ticker]["type"] = iType
 616
 617                if "minPriceIncrement" in iList[iType][ticker].keys():
 618                    iList[iType][ticker]["step"] = NanoToFloat(
 619                        iList[iType][ticker]["minPriceIncrement"]["units"],
 620                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 621                    )
 622
 623                else:
 624                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 625
 626        return iList
 627
 628    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 629        """
 630        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 631
 632        See also: `DumpInstruments()`, `Listing()`.
 633
 634        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 635                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 636        """
 637        if self.iListDumpFile is None or not self.iListDumpFile:
 638            uLogger.error("Output name of dump file must be defined!")
 639            raise Exception("Filename required")
 640
 641        if not self.iList or forceUpdate:
 642            self.iList = self.Listing()
 643
 644        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 645
 646        # Save as XLSX with separated sheets for every type of instruments:
 647        with pd.ExcelWriter(
 648                path=xlsxDumpFile,
 649                date_format=TKS_DATE_FORMAT,
 650                datetime_format=TKS_DATE_TIME_FORMAT,
 651                mode="w",
 652        ) as writer:
 653            for iType in TKS_INSTRUMENTS:
 654                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 655                df = df[sorted(df)]  # sorted by column names
 656                df = df.applymap(
 657                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 658                    na_action="ignore",
 659                )  # converting numbers from nano-type to float in every cell
 660                df.to_excel(
 661                    writer,
 662                    sheet_name=iType,
 663                    encoding="UTF-8",
 664                    freeze_panes=(1, 1),
 665                )  # saving as XLSX-file with freeze first row and column as headers
 666
 667        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 668
 669    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 670        """
 671        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 672        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 673
 674        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 675
 676        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 677                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 678        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 679        """
 680        if self.iListDumpFile is None or not self.iListDumpFile:
 681            uLogger.error("Output name of dump file must be defined!")
 682            raise Exception("Filename required")
 683
 684        if not self.iList or forceUpdate:
 685            self.iList = self.Listing()
 686
 687        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 688        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 689            fH.write(jsonDump)
 690
 691        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 692
 693        return jsonDump
 694
 695    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 696        """
 697        Show information about one instrument defined by json data and prints it in Markdown format.
 698
 699        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 700
 701        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 702        :param show: if `True` then also printing information about instrument and its current price.
 703        :return: multilines text in Markdown format with information about one instrument.
 704        """
 705        splitLine = "|                                                             |                                                        |\n"
 706        infoText = ""
 707
 708        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 709            info = [
 710                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 711                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 712                "| Parameters                                                  | Values                                                 |\n",
 713                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 714                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 715                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 716            ]
 717
 718            if "sector" in iJSON.keys() and iJSON["sector"]:
 719                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 720
 721            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 722                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 723                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 724            )))
 725
 726            info.extend([
 727                splitLine,
 728                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 729                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 730            ])
 731
 732            if "isin" in iJSON.keys() and iJSON["isin"]:
 733                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 734
 735            if "classCode" in iJSON.keys():
 736                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 737
 738            info.extend([
 739                splitLine,
 740                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 741                splitLine,
 742                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 743                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 744                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 745            ])
 746
 747            if iJSON["figi"]:
 748                self.figi = iJSON["figi"]
 749                iJSON = iJSON | self.RequestTradingStatus()
 750
 751                info.extend([
 752                    splitLine,
 753                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 754                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 755                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 756                ])
 757
 758            info.append(splitLine)
 759
 760            if "type" in iJSON.keys() and iJSON["type"]:
 761                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 762
 763            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 764                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 765
 766            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 767                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 768
 769            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 770                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 771
 772            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 773                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 774
 775            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 776                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 777
 778            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 779                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 780
 781            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 782                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 783
 784            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 785                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 786
 787            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 788                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 789
 790            if "currency" in iJSON.keys():
 791                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 792
 793            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 794                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 795
 796            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 797                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 798
 799            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 800                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 801
 802            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 803                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 804
 805            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 806                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 807
 808            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 809                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 810
 811            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 812                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 813
 814            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 815                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 816
 817            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 818                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 819
 820            iExt = None
 821            if iJSON["type"] == "Bonds":
 822                info.extend([
 823                    splitLine,
 824                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 825                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 826                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 827                        iJSON["nominal"]["currency"],
 828                    )),
 829                ])
 830
 831                if "floatingCouponFlag" in iJSON.keys():
 832                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 833
 834                if "amortizationFlag" in iJSON.keys():
 835                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 836
 837                info.append(splitLine)
 838
 839                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 840                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 841
 842                iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 843
 844                info.extend([
 845                    "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 846                    "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 847                    "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 848                ])
 849
 850                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 851                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 852                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 853                        iJSON["aciValue"]["currency"]
 854                    )))
 855
 856            if "currentPrice" in iJSON.keys():
 857                info.append(splitLine)
 858
 859                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 860                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 861
 862                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 863                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 864                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 865                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 866                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 867
 868                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 869                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 870
 871                info.extend([
 872                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 873                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 874                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 875                    )),
 876                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 877                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 878                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 879                    )),
 880                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 881                        "{:.2f}%{}".format(
 882                            iJSON["currentPrice"]["changes"],
 883                            " ({}{:.2f} {})".format(
 884                                "+" if bondChangesDelta > 0 else "",
 885                                bondChangesDelta,
 886                                aciCurrency
 887                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 888                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 889                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 890                                currency
 891                            ),
 892                        )
 893                    ),
 894                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 895                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 896                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 897                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 898                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 899                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 900                    )),
 901                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 902                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 903                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 904                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 905                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 906                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 907                    )),
 908                ])
 909
 910            if "lot" in iJSON.keys():
 911                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 912
 913            if "step" in iJSON.keys() and iJSON["step"] != 0:
 914                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
 915
 916            # Add bond payment calendar:
 917            if iJSON["type"] == "Bonds":
 918                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 919                info.extend(["\n", strCalendar])
 920
 921            infoText += "".join(info)
 922
 923            if show:
 924                uLogger.info("{}".format(infoText))
 925
 926            else:
 927                uLogger.debug("{}".format(infoText))
 928
 929            if self.infoFile is not None:
 930                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 931                    fH.write(infoText)
 932
 933                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 934
 935        return infoText
 936
 937    def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 938        """
 939        Search and return raw broker's information about instrument by its ticker.
 940        `ticker` must be defined! If debug=True then print all debug messages.
 941
 942        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 943        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 944        :param debug: if `True` then print all debug console messages.
 945        :return: JSON formatted data with information about instrument.
 946        """
 947        tickerJSON = {}
 948        if debug:
 949            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 950
 951        if not self.ticker:
 952            uLogger.warning("self.ticker variable is not be empty!")
 953
 954        else:
 955            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 956                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 957                raise Exception("Instrument not allowed")
 958
 959            if not self.iList:
 960                self.iList = self.Listing()
 961
 962            if self.ticker in self.iList["Shares"].keys():
 963                tickerJSON = self.iList["Shares"][self.ticker]
 964                if debug:
 965                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 966
 967            elif self.ticker in self.iList["Currencies"].keys():
 968                tickerJSON = self.iList["Currencies"][self.ticker]
 969                if debug:
 970                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 971
 972            elif self.ticker in self.iList["Bonds"].keys():
 973                tickerJSON = self.iList["Bonds"][self.ticker]
 974                if debug:
 975                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 976
 977            elif self.ticker in self.iList["Etfs"].keys():
 978                tickerJSON = self.iList["Etfs"][self.ticker]
 979                if debug:
 980                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 981
 982            elif self.ticker in self.iList["Futures"].keys():
 983                tickerJSON = self.iList["Futures"][self.ticker]
 984                if debug:
 985                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 986
 987        if tickerJSON:
 988            self.figi = tickerJSON["figi"]
 989
 990            if requestPrice:
 991                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 992
 993                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 994                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 995
 996                else:
 997                    tickerJSON["currentPrice"]["changes"] = 0
 998
 999            if show:
1000                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
1001
1002        else:
1003            if show:
1004                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1005
1006        return tickerJSON
1007
1008    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1009        """
1010        Search and return raw broker's information about instrument by its FIGI.
1011        `figi` must be defined! If debug=True then print all debug messages.
1012
1013        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1014        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1015        :param debug: if `True` then print all debug console messages.
1016        :return: JSON formatted data with information about instrument.
1017        """
1018        figiJSON = {}
1019        if debug:
1020            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1021
1022        if not self.figi:
1023            uLogger.warning("self.figi variable is not be empty!")
1024
1025        else:
1026            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1027                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1028                raise Exception("Instrument not allowed")
1029
1030            if not self.iList:
1031                self.iList = self.Listing()
1032
1033            for item in self.iList["Shares"].keys():
1034                if self.figi == self.iList["Shares"][item]["figi"]:
1035                    figiJSON = self.iList["Shares"][item]
1036
1037                    if debug:
1038                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1039
1040                    break
1041
1042            if not figiJSON:
1043                for item in self.iList["Currencies"].keys():
1044                    if self.figi == self.iList["Currencies"][item]["figi"]:
1045                        figiJSON = self.iList["Currencies"][item]
1046
1047                        if debug:
1048                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1049
1050                        break
1051
1052            if not figiJSON:
1053                for item in self.iList["Bonds"].keys():
1054                    if self.figi == self.iList["Bonds"][item]["figi"]:
1055                        figiJSON = self.iList["Bonds"][item]
1056
1057                        if debug:
1058                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1059
1060                        break
1061
1062            if not figiJSON:
1063                for item in self.iList["Etfs"].keys():
1064                    if self.figi == self.iList["Etfs"][item]["figi"]:
1065                        figiJSON = self.iList["Etfs"][item]
1066
1067                        if debug:
1068                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1069
1070                        break
1071
1072            if not figiJSON:
1073                for item in self.iList["Futures"].keys():
1074                    if self.figi == self.iList["Futures"][item]["figi"]:
1075                        figiJSON = self.iList["Futures"][item]
1076
1077                        if debug:
1078                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1079
1080                        break
1081
1082        if figiJSON:
1083            self.figi = figiJSON["figi"]
1084            self.ticker = figiJSON["ticker"]
1085
1086            if requestPrice:
1087                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1088
1089                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1090                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1091
1092                else:
1093                    figiJSON["currentPrice"]["changes"] = 0
1094
1095            if show:
1096                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1097
1098        else:
1099            if show:
1100                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1101
1102        return figiJSON
1103
1104    def GetCurrentPrices(self, show: bool = True) -> dict:
1105        """
1106        Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
1107        `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1108
1109        See also:
1110
1111        :param show: if `True` then print DOM to log and console.
1112        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1113        """
1114        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1115
1116        if self.depth < 1:
1117            uLogger.error("Depth of Market (DOM) must be >=1!")
1118            raise Exception("Incorrect value")
1119
1120        if not (self.ticker or self.figi):
1121            uLogger.error("self.ticker or self.figi variables must be defined!")
1122            raise Exception("Ticker or FIGI required")
1123
1124        if self.ticker and not self.figi:
1125            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1126            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1127
1128        if not self.ticker and self.figi:
1129            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1130            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1131
1132        if not self.figi:
1133            uLogger.error("FIGI is not defined!")
1134            raise Exception("Ticker or FIGI required")
1135
1136        else:
1137            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1138
1139            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1140            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1141            self.body = str({"figi": self.figi, "depth": self.depth})
1142            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")
1143
1144            if pricesResponse:
1145                # list of dicts with sellers orders:
1146                prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1147
1148                # list of dicts with buyers orders:
1149                prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1150
1151                # max price of instrument at this time:
1152                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1153
1154                # min price of instrument at this time:
1155                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1156
1157                # last price of deal with instrument:
1158                prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0
1159
1160                # last close price of instrument:
1161                prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0
1162
1163            else:
1164                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1165                uLogger.debug("Server response: {}".format(pricesResponse))
1166
1167            if show:
1168                if prices["buy"] or prices["sell"]:
1169                    info = [
1170                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1171                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1172                            self.ticker,
1173                            self.figi,
1174                            self.depth,
1175                        ),
1176                        uLog.sepShort, "\n",
1177                        " Orders of Buyers   | Orders of Sellers\n",
1178                        uLog.sepShort, "\n",
1179                        " Sell prices (vol.) | Buy prices (vol.)\n",
1180                        uLog.sepShort, "\n",
1181                    ]
1182
1183                    if not prices["buy"]:
1184                        info.append("                    | No orders!\n")
1185                        sumBuy = 0
1186
1187                    else:
1188                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1189                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1190                        for item in maxMinSorted:
1191                            info.append("                    | {} ({})\n".format(item["price"], item["quantity"]))
1192
1193                    if not prices["sell"]:
1194                        info.append("No orders!          |\n")
1195                        sumSell = 0
1196
1197                    else:
1198                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1199                        for item in prices["sell"]:
1200                            info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1201
1202                    info.extend([
1203                        uLog.sepShort, "\n",
1204                        "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1205                        uLog.sepShort, "\n",
1206                    ])
1207
1208                    infoText = "".join(info)
1209
1210                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1211
1212                else:
1213                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1214
1215        return prices
1216
1217    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1218        """
1219        This method get and show information about all available broker instruments for current user account.
1220        If `instrumentsFile` string is not empty then also save information to this file.
1221
1222        :param show: if `True` then print results to console, if `False` - print only to file.
1223        :return: multi-lines string with all available broker instruments
1224        """
1225        if not self.iList:
1226            self.iList = self.Listing()
1227
1228        info = [
1229            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1230            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1231        ]
1232
1233        # add instruments count by type:
1234        for iType in self.iList.keys():
1235            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1236
1237        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1238        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1239
1240        # generating info tables with all instruments by type:
1241        for iType in self.iList.keys():
1242            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1243
1244            for instrument in self.iList[iType].keys():
1245                iName = self.iList[iType][instrument]["name"]  # instrument's name
1246                if len(iName) > 57:
1247                    iName = "{}...".format(iName[:54])  # right trim for a long string
1248
1249                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1250                    self.iList[iType][instrument]["ticker"],
1251                    iName,
1252                    self.iList[iType][instrument]["figi"],
1253                    self.iList[iType][instrument]["currency"],
1254                    self.iList[iType][instrument]["lot"],
1255                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1256                ))
1257
1258        infoText = "".join(info)
1259
1260        if show:
1261            uLogger.info(infoText)
1262
1263        if self.instrumentsFile:
1264            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1265                fH.write(infoText)
1266
1267            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1268
1269        return infoText
1270
1271    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1272        """
1273        This method search and show information about instruments by part of its ticker, FIGI or name.
1274        If `searchResultsFile` string is not empty then also save information to this file.
1275
1276        :param pattern: string with part of ticker, FIGI or instrument's name.
1277        :param show: if `True` then print results to console, if `False` - return list of result only.
1278        :return: list of dictionaries with all found instruments.
1279        """
1280        if not self.iList:
1281            self.iList = self.Listing()
1282
1283        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1284        compiledPattern = re.compile(pattern, re.IGNORECASE)
1285
1286        for iType in self.iList:
1287            for instrument in self.iList[iType].values():
1288                searchResult = compiledPattern.search(" ".join(
1289                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1290                ))
1291
1292                if searchResult:
1293                    searchResults[iType][instrument["ticker"]] = instrument
1294
1295        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1296        info = [
1297            "# Search results\n\n",
1298            "* **Search pattern:** [{}]\n".format(pattern),
1299            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1300            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1301        ]
1302        infoShort = info[:]
1303
1304        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1305        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1306        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1307
1308        if resultsLen == 0:
1309            info.append("\nNo results\n")
1310            infoShort.append("\nNo results\n")
1311            uLogger.warning("No results. Try changing your search pattern.")
1312
1313        else:
1314            for iType in searchResults:
1315                iTypeValuesCount = len(searchResults[iType].values())
1316                if iTypeValuesCount > 0:
1317                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1318                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1319
1320                    for instrument in searchResults[iType].values():
1321                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1322                            instrument["type"],
1323                            instrument["ticker"],
1324                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1325                            instrument["figi"],
1326                        ))
1327
1328                    if iTypeValuesCount <= 5:
1329                        infoShort.extend(info[-iTypeValuesCount:])
1330
1331                    else:
1332                        infoShort.extend(info[-5:])
1333                        infoShort.append(skippedLine)
1334
1335        infoText = "".join(info)
1336        infoTextShort = "".join(infoShort)
1337
1338        if show:
1339            uLogger.info(infoTextShort)
1340            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1341
1342        if self.searchResultsFile:
1343            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1344                fH.write(infoText)
1345
1346            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1347
1348        return searchResults
1349
1350    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1351        """
1352        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1353
1354        :param instruments: list of strings with tickers or FIGIs.
1355        :return: list with unique instrument FIGIs only.
1356        """
1357        requestedInstruments = []
1358        for iName in instruments:
1359            if iName not in self.aliases.keys():
1360                if iName not in requestedInstruments:
1361                    requestedInstruments.append(iName)
1362
1363            else:
1364                if iName not in requestedInstruments:
1365                    if self.aliases[iName] not in requestedInstruments:
1366                        requestedInstruments.append(self.aliases[iName])
1367
1368        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1369
1370        onlyUniqueFIGIs = []
1371        for iName in requestedInstruments:
1372            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1373                continue
1374
1375            self.ticker = iName
1376            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1377
1378            if not iData:
1379                self.ticker = ""
1380                self.figi = iName
1381
1382                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1383
1384                if not iData:
1385                    self.figi = ""
1386                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1387
1388            if iData and iData["figi"] not in onlyUniqueFIGIs:
1389                onlyUniqueFIGIs.append(iData["figi"])
1390
1391        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1392
1393        return onlyUniqueFIGIs
1394
1395    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1396        """
1397        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1398        See limits: https://tinkoff.github.io/investAPI/limits/
1399        If `pricesFile` string is not empty then also save information to this file.
1400
1401        :param instruments: list of strings with tickers or FIGIs.
1402        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1403        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1404                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1405        """
1406        if instruments is None or not instruments:
1407            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1408            raise Exception("Ticker or FIGI required")
1409
1410        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1411
1412        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1413
1414        iList = []  # trying to get info and current prices about all unique instruments:
1415        for self.figi in onlyUniqueFIGIs:
1416            iData = self.SearchByFIGI(requestPrice=True)
1417            iList.append(iData)
1418
1419        self.ShowListOfPrices(iList, show)
1420
1421        return iList
1422
1423    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1424        """
1425        Show table contains current prices of given instruments.
1426
1427        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1428                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1429        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1430        :return: multilines text in Markdown format as a table contains current prices.
1431        """
1432        infoText = ""
1433
1434        if show or self.pricesFile:
1435            info = [
1436                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1437                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1438                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1439            ]
1440
1441            for item in iList:
1442                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1443                    item["ticker"],
1444                    item["figi"],
1445                    item["type"],
1446                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1447                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1448                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1449                    "{} / {}".format(
1450                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1451                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1452                    ),
1453                    "{} / {}".format(
1454                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1455                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1456                    ),
1457                    item["currency"],
1458                ))
1459
1460            infoText = "".join(info)
1461
1462            if show:
1463                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1464
1465            if self.pricesFile:
1466                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1467                    fH.write(infoText)
1468
1469                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1470
1471        return infoText
1472
1473    def RequestTradingStatus(self) -> dict:
1474        """
1475        Requesting trading status for the instrument defined by `figi` variable.
1476        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1477        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1478
1479        :return: dictionary with trading status attributes. Response example:
1480                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1481                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1482        """
1483        if self.figi is None or not self.figi:
1484            uLogger.error("Variable `figi` must be defined for using this method!")
1485            raise Exception("FIGI required")
1486
1487        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1488
1489        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1490        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1491        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1492
1493        uLogger.debug("Records about current trading status successfully received")
1494
1495        return tradingStatus
1496
1497    def RequestPortfolio(self) -> dict:
1498        """
1499        Requesting actual user's portfolio for current `accountId`.
1500        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1501        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1502
1503        :return: dictionary with user's portfolio.
1504        """
1505        if self.accountId is None or not self.accountId:
1506            uLogger.error("Variable `accountId` must be defined for using this method!")
1507            raise Exception("Account ID required")
1508
1509        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1510
1511        self.body = str({"accountId": self.accountId})
1512        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1513        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1514
1515        uLogger.debug("Records about user's portfolio successfully received")
1516
1517        return rawPortfolio
1518
1519    def RequestPositions(self) -> dict:
1520        """
1521        Requesting open positions by currencies and instruments for current `accountId`.
1522        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1523        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1524
1525        :return: dictionary with open positions by instruments.
1526        """
1527        if self.accountId is None or not self.accountId:
1528            uLogger.error("Variable `accountId` must be defined for using this method!")
1529            raise Exception("Account ID required")
1530
1531        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1532
1533        self.body = str({"accountId": self.accountId})
1534        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1535        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1536
1537        uLogger.debug("Records about current open positions successfully received")
1538
1539        return rawPositions
1540
1541    def RequestPendingOrders(self) -> list:
1542        """
1543        Requesting current actual pending orders for current `accountId`.
1544        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1545        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1546
1547        :return: list of dictionaries with pending orders.
1548        """
1549        if self.accountId is None or not self.accountId:
1550            uLogger.error("Variable `accountId` must be defined for using this method!")
1551            raise Exception("Account ID required")
1552
1553        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1554
1555        self.body = str({"accountId": self.accountId})
1556        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1557        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1558
1559        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1560
1561        return rawOrders
1562
1563    def RequestStopOrders(self) -> list:
1564        """
1565        Requesting current actual stop orders for current `accountId`.
1566        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1567        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1568
1569        :return: list of dictionaries with stop orders.
1570        """
1571        if self.accountId is None or not self.accountId:
1572            uLogger.error("Variable `accountId` must be defined for using this method!")
1573            raise Exception("Account ID required")
1574
1575        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1576
1577        self.body = str({"accountId": self.accountId})
1578        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1579        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1580
1581        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1582
1583        return rawStopOrders
1584
1585    def Overview(self, show: bool = False, details: str = "full") -> dict:
1586        """
1587        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1588        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1589        are defined then also save information to file.
1590
1591        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1592        many requests about the state of the portfolio, and then, based on the received data, a large number
1593        of calculation and statistics are collected.
1594
1595        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1596        :param details: how detailed should the information be? You should specify one of strings:
1597                        `full` - shows full available information about portfolio status (by default),
1598                        `positions` - shows only open positions,
1599                        `digest` - show a short digest of the portfolio status,
1600                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1601                        `orders` - shows only sections of open limits and stop orders.
1602        :return: dictionary with client's raw portfolio and some statistics.
1603        """
1604        if self.accountId is None or not self.accountId:
1605            uLogger.error("Variable `accountId` must be defined for using this method!")
1606            raise Exception("Account ID required")
1607
1608        view = {
1609            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1610                "headers": {},  # list of dictionaries, response headers without "positions" section
1611                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1612                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1613                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1614                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1615                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1616                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1617                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1618                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1619                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1620            },
1621            "stat": {  # --- some statistics calculated using "raw" sections:
1622                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1623                "availableRUB": 0.,  # available rubles (without other currencies)
1624                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1625                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1626                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1627                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1628                "sharesCostRUB": 0.,  # costs of all shares in RUB
1629                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1630                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1631                "futuresCostRUB": 0.,  # costs of all futures in RUB
1632                "Currencies": [],  # list of dictionaries of all currencies statistics
1633                "Shares": [],  # list of dictionaries of all shares statistics
1634                "Bonds": [],  # list of dictionaries of all bonds statistics
1635                "Etfs": [],  # list of dictionaries of all etfs statistics
1636                "Futures": [],  # list of dictionaries of all futures statistics
1637                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1638                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1639                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1640                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1641                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1642            },
1643            "analytics": {  # --- some analytics of portfolio:
1644                "distrByAssets": {},  # portfolio distribution by assets
1645                "distrByCompanies": {},  # portfolio distribution by companies
1646                "distrBySectors": {},  # portfolio distribution by sectors
1647                "distrByCurrencies": {},  # portfolio distribution by currencies
1648                "distrByCountries": {},  # portfolio distribution by countries
1649            }
1650        }
1651
1652        details = details.lower()
1653        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1654        if details not in availableDetails:
1655            details = "full"
1656            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1657
1658        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1659
1660        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1661        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1662        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1663        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1664
1665        # save response headers without "positions" section:
1666        for key in portfolioResponse.keys():
1667            if key != "positions":
1668                view["raw"]["headers"][key] = portfolioResponse[key]
1669
1670            else:
1671                continue
1672
1673        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1674        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1675        for item in portfolioResponse["positions"]:
1676            if item["instrumentType"] == "currency":
1677                self.figi = item["figi"]
1678                curr = self.SearchByFIGI(requestPrice=False)
1679
1680                # current price of currency in RUB:
1681                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1682                    "name": curr["name"],
1683                    "currentPrice": NanoToFloat(
1684                        item["currentPrice"]["units"],
1685                        item["currentPrice"]["nano"]
1686                    ),
1687                }
1688
1689                view["raw"]["Currencies"].append(item)
1690
1691            elif item["instrumentType"] == "share":
1692                view["raw"]["Shares"].append(item)
1693
1694            elif item["instrumentType"] == "bond":
1695                view["raw"]["Bonds"].append(item)
1696
1697            elif item["instrumentType"] == "etf":
1698                view["raw"]["Etfs"].append(item)
1699
1700            elif item["instrumentType"] == "futures":
1701                view["raw"]["Futures"].append(item)
1702
1703            else:
1704                continue
1705
1706        # how many volume of currencies (by ISO currency name) are blocked:
1707        for item in view["raw"]["positions"]["blocked"]:
1708            blocked = NanoToFloat(item["units"], item["nano"])
1709            if blocked > 0:
1710                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1711
1712        # how many volume of instruments (by FIGI) are blocked:
1713        for item in view["raw"]["positions"]["securities"]:
1714            blocked = int(item["blocked"])
1715            if blocked > 0:
1716                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1717
1718        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1719
1720        if "rub" in allBlocked.keys():
1721            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1722
1723        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1724        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1725        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1726        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1727        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1728        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1729        view["stat"]["portfolioCostRUB"] = sum([
1730            view["stat"]["allCurrenciesCostRUB"],
1731            view["stat"]["sharesCostRUB"],
1732            view["stat"]["bondsCostRUB"],
1733            view["stat"]["etfsCostRUB"],
1734            view["stat"]["futuresCostRUB"],
1735        ])
1736
1737        # --- calculating some portfolio statistics:
1738        byComp = {}  # distribution by companies
1739        bySect = {}  # distribution by sectors
1740        byCurr = {}  # distribution by currencies (include RUB)
1741        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1742        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1743
1744        for item in portfolioResponse["positions"]:
1745            self.figi = item["figi"]
1746            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1747
1748            if instrument:
1749                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1750                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1751
1752                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1753                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1754
1755                else:
1756                    blocked = 0
1757
1758                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1759                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1760                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1761                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1762                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1763                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1764                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1765                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1766                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1767                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1768                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1769                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1770
1771                statData = {
1772                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1773                    "ticker": instrument["ticker"],  # ticker by FIGI
1774                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1775                    "volume": volume,  # available volume of instrument
1776                    "lots": lots,  # volume in lots of instrument
1777                    "direction": direction,  # direction of an instrument's position: short or long
1778                    "blocked": blocked,  # blocked volume of currency or instrument
1779                    "currentPrice": curPrice,  # current instrument's price in basic asset
1780                    "average": average,  # current average position price
1781                    "cost": cost,  # current cost of all volume of instrument in basic asset
1782                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1783                    "costRUB": costRUB,  # cost of instrument in ruble
1784                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1785                    "profit": profit,  # expected profit at current moment
1786                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1787                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1788                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1789                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1790                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1791                    "step": instrument["step"],  # minimum price increment
1792                }
1793
1794                # adding distribution by unique countries:
1795                if statData["country"] not in byCountry.keys():
1796                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1797
1798                else:
1799                    byCountry[statData["country"]]["cost"] += costRUB
1800                    byCountry[statData["country"]]["percent"] += percentCostRUB
1801
1802                if item["instrumentType"] != "currency":
1803                    # adding distribution by unique companies:
1804                    if statData["name"]:
1805                        if statData["name"] not in byComp.keys():
1806                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1807
1808                        else:
1809                            byComp[statData["name"]]["cost"] += costRUB
1810                            byComp[statData["name"]]["percent"] += percentCostRUB
1811
1812                    # adding distribution by unique sectors:
1813                    if statData["sector"] not in bySect.keys():
1814                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1815
1816                    else:
1817                        bySect[statData["sector"]]["cost"] += costRUB
1818                        bySect[statData["sector"]]["percent"] += percentCostRUB
1819
1820                # adding distribution by unique currencies:
1821                if currency not in byCurr.keys():
1822                    byCurr[currency] = {
1823                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1824                        "cost": costRUB,
1825                        "percent": percentCostRUB
1826                    }
1827
1828                else:
1829                    byCurr[currency]["cost"] += costRUB
1830                    byCurr[currency]["percent"] += percentCostRUB
1831
1832                # saving statistics for every instrument:
1833                if item["instrumentType"] == "currency":
1834                    view["stat"]["Currencies"].append(statData)
1835
1836                    # update dict with free funds for trading (total - blocked) by currencies
1837                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1838                    view["stat"]["funds"][currency] = {
1839                        "total": volume,
1840                        "totalCostRUB": costRUB,  # total volume cost in rubles
1841                        "free": volume - blocked,
1842                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1843                    }
1844
1845                elif item["instrumentType"] == "share":
1846                    view["stat"]["Shares"].append(statData)
1847
1848                elif item["instrumentType"] == "bond":
1849                    view["stat"]["Bonds"].append(statData)
1850
1851                elif item["instrumentType"] == "etf":
1852                    view["stat"]["Etfs"].append(statData)
1853
1854                elif item["instrumentType"] == "Futures":
1855                    view["stat"]["Futures"].append(statData)
1856
1857                else:
1858                    continue
1859
1860        # total changes in Russian Ruble:
1861        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1862        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1863        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1864        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1865        view["stat"]["funds"]["rub"] = {
1866            "total": view["stat"]["availableRUB"],
1867            "totalCostRUB": view["stat"]["availableRUB"],
1868            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1869            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1870        }
1871
1872        # --- pending orders sector data:
1873        uniquePendingOrders = []
1874        uniquePendingOrdersFIGIs = []
1875        for item in view["raw"]["orders"]:
1876            if item["figi"] not in uniquePendingOrdersFIGIs:
1877                uniquePendingOrdersFIGIs.append(item["figi"])
1878                uniquePendingOrders.append(item)
1879
1880        for item in uniquePendingOrders:
1881            self.figi = item["figi"]
1882            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1883
1884            if instrument:
1885                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1886                orderType = TKS_ORDER_TYPES[item["orderType"]]
1887                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1888                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1889
1890                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1891                if item["direction"] == "ORDER_DIRECTION_BUY":
1892                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1893
1894                else:
1895                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1896
1897                # requested price for order execution:
1898                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1899
1900                # necessary changes in percent to reach target from current price:
1901                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1902
1903                view["stat"]["orders"].append({
1904                    "orderID": item["orderId"],  # orderId number parameter of current order
1905                    "figi": item["figi"],  # FIGI identification
1906                    "ticker": instrument["ticker"],  # ticker name by FIGI
1907                    "lotsRequested": item["lotsRequested"],  # requested lots value
1908                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1909                    "currentPrice": lastPrice,  # current instrument's price for defined action
1910                    "targetPrice": target,  # requested price for order execution in base currency
1911                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1912                    "percentChanges": changes,  # changes in percent to target from current price
1913                    "currency": item["currency"],  # instrument's currency name
1914                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1915                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1916                    "status": orderState,  # order status from TKS_ORDER_STATES
1917                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1918                })
1919
1920        # --- stop orders sector data:
1921        uniqueStopOrders = []
1922        uniqueStopOrdersFIGIs = []
1923        for item in view["raw"]["stopOrders"]:
1924            if item["figi"] not in uniqueStopOrdersFIGIs:
1925                uniqueStopOrdersFIGIs.append(item["figi"])
1926                uniqueStopOrders.append(item)
1927
1928        for item in uniqueStopOrders:
1929            self.figi = item["figi"]
1930            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1931
1932            if instrument:
1933                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1934                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1935                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1936
1937                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1938                if "expirationTime" in item.keys():
1939                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1940                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1941
1942                else:
1943                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1944                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1945
1946                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1947                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1948                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1949
1950                else:
1951                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1952
1953                # requested price when stop-order executed:
1954                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1955
1956                # price for limit-order, set up when stop-order executed:
1957                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1958
1959                # necessary changes in percent to reach target from current price:
1960                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1961
1962                view["stat"]["stopOrders"].append({
1963                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1964                    "figi": item["figi"],  # FIGI identification
1965                    "ticker": instrument["ticker"],  # ticker name by FIGI
1966                    "lotsRequested": item["lotsRequested"],  # requested lots value
1967                    "currentPrice": lastPrice,  # current instrument's price for defined action
1968                    "targetPrice": target,  # requested price for stop-order execution in base currency
1969                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1970                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1971                    "percentChanges": changes,  # changes in percent to target from current price
1972                    "currency": item["currency"],  # instrument's currency name
1973                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1974                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1975                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1976                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1977                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1978                })
1979
1980        # --- calculating data for analytics section:
1981        # portfolio distribution by assets:
1982        view["analytics"]["distrByAssets"] = {
1983            "Ruble": {
1984                "uniques": 1,
1985                "cost": view["stat"]["availableRUB"],
1986                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1987            },
1988            "Currencies": {
1989                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1990                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1991                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1992            },
1993            "Shares": {
1994                "uniques": len(view["stat"]["Shares"]),
1995                "cost": view["stat"]["sharesCostRUB"],
1996                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1997            },
1998            "Bonds": {
1999                "uniques": len(view["stat"]["Bonds"]),
2000                "cost": view["stat"]["bondsCostRUB"],
2001                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2002            },
2003            "Etfs": {
2004                "uniques": len(view["stat"]["Etfs"]),
2005                "cost": view["stat"]["etfsCostRUB"],
2006                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2007            },
2008            "Futures": {
2009                "uniques": len(view["stat"]["Futures"]),
2010                "cost": view["stat"]["futuresCostRUB"],
2011                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2012            },
2013        }
2014
2015        # portfolio distribution by companies:
2016        view["analytics"]["distrByCompanies"]["All money cash"] = {
2017            "ticker": "",
2018            "cost": view["stat"]["allCurrenciesCostRUB"],
2019            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2020        }
2021        view["analytics"]["distrByCompanies"].update(byComp)
2022
2023        # portfolio distribution by sectors:
2024        view["analytics"]["distrBySectors"]["All money cash"] = {
2025            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2026            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2027        }
2028        view["analytics"]["distrBySectors"].update(bySect)
2029
2030        # portfolio distribution by currencies:
2031        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2032            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2033            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2034
2035        view["analytics"]["distrByCurrencies"].update(byCurr)
2036        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2037        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2038
2039        # portfolio distribution by countries:
2040        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2041            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2042            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2043
2044        view["analytics"]["distrByCountries"].update(byCountry)
2045        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2046        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2047
2048        # --- Prepare text statistics overview in human-readable:
2049        if show:
2050            # Whatever the value `details`, header not changes:
2051            info = [
2052                "# Client's portfolio\n\n",
2053                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2054                "* **Account ID:** [{}]\n".format(self.accountId),
2055            ]
2056
2057            if details in ["full", "positions", "digest"]:
2058                info.extend([
2059                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2060                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2061                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2062                        view["stat"]["totalChangesRUB"],
2063                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2064                        view["stat"]["totalChangesPercentRUB"],
2065                    ),
2066                ])
2067
2068            if details in ["full", "positions"]:
2069                info.extend([
2070                    "## Open positions\n\n",
2071                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2072                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2073                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2074                        "{:.2f} ({:.2f}) rub".format(
2075                            view["stat"]["availableRUB"],
2076                            view["stat"]["blockedRUB"],
2077                        )
2078                    )
2079                ])
2080
2081                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2082                    return [
2083                        "|                             |                                 |          |              |              |                     |                              |\n",
2084                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2085                            noTradeStr if noTradeStr else typeStr,
2086                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2087                        ),
2088                    ]
2089
2090                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2091                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2092                        "{} [{}]".format(data["ticker"], data["figi"]),
2093                        "{:.2f} ({:.2f}) {}".format(
2094                            data["volume"],
2095                            data["blocked"],
2096                            data["currency"],
2097                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2098                            data["volume"],
2099                            data["blocked"],
2100                        ),
2101                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2102                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2103                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2104                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2105                        "{}{:.2f} {} ({}{:.2f}%)".format(
2106                            "+" if data["profit"] > 0 else "",
2107                            data["profit"], data["baseCurrencyName"],
2108                            "+" if data["percentProfit"] > 0 else "",
2109                            data["percentProfit"],
2110                        ),
2111                    )
2112
2113                # --- Show currencies section:
2114                if view["stat"]["Currencies"]:
2115                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2116                    for item in view["stat"]["Currencies"]:
2117                        info.append(_InfoStr(item, showCurrencyName=True))
2118
2119                else:
2120                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2121
2122                # --- Show shares section:
2123                if view["stat"]["Shares"]:
2124                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2125
2126                    for item in view["stat"]["Shares"]:
2127                        info.append(_InfoStr(item))
2128
2129                else:
2130                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2131
2132                # --- Show bonds section:
2133                if view["stat"]["Bonds"]:
2134                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2135
2136                    for item in view["stat"]["Bonds"]:
2137                        info.append(_InfoStr(item))
2138
2139                else:
2140                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2141
2142                # --- Show etfs section:
2143                if view["stat"]["Etfs"]:
2144                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2145
2146                    for item in view["stat"]["Etfs"]:
2147                        info.append(_InfoStr(item))
2148
2149                else:
2150                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2151
2152                # --- Show futures section:
2153                if view["stat"]["Futures"]:
2154                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2155
2156                    for item in view["stat"]["Futures"]:
2157                        info.append(_InfoStr(item))
2158
2159                else:
2160                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2161
2162            if details in ["full", "orders"]:
2163                # --- Show pending orders section:
2164                if view["stat"]["orders"]:
2165                    info.extend([
2166                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2167                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2168                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2169                    ])
2170
2171                    for item in view["stat"]["orders"]:
2172                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2173                            "{} [{}]".format(item["ticker"], item["figi"]),
2174                            item["orderID"],
2175                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2176                            "{} {} ({}{:.2f}%)".format(
2177                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2178                                item["baseCurrencyName"],
2179                                "+" if item["percentChanges"] > 0 else "",
2180                                float(item["percentChanges"]),
2181                            ),
2182                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2183                            item["action"],
2184                            item["type"],
2185                            item["date"],
2186                        ))
2187
2188                else:
2189                    info.append("\n## Total pending limit-orders: 0\n")
2190
2191                # --- Show stop orders section:
2192                if view["stat"]["stopOrders"]:
2193                    info.extend([
2194                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2195                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2196                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2197                    ])
2198
2199                    for item in view["stat"]["stopOrders"]:
2200                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2201                            "{} [{}]".format(item["ticker"], item["figi"]),
2202                            item["orderID"],
2203                            item["lotsRequested"],
2204                            "{} {} ({}{:.2f}%)".format(
2205                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2206                                item["baseCurrencyName"],
2207                                "+" if item["percentChanges"] > 0 else "",
2208                                float(item["percentChanges"]),
2209                            ),
2210                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2211                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2212                            item["action"],
2213                            item["type"],
2214                            item["expType"],
2215                            item["createDate"],
2216                            item["expDate"],
2217                        ))
2218
2219                else:
2220                    info.append("\n## Total stop-orders: 0\n")
2221
2222            if details in ["full", "analytics"]:
2223                # -- Show analytics section:
2224                if view["stat"]["portfolioCostRUB"] > 0:
2225                    info.extend([
2226                        "\n# Analytics\n"
2227                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2228                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2229                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2230                            view["stat"]["totalChangesRUB"],
2231                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2232                            view["stat"]["totalChangesPercentRUB"],
2233                        ),
2234                        "\n## Portfolio distribution by assets\n"
2235                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2236                        "|------------|---------|---------|--------------------|\n",
2237                    ])
2238
2239                    for key in view["analytics"]["distrByAssets"].keys():
2240                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2241                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2242                                key,
2243                                view["analytics"]["distrByAssets"][key]["uniques"],
2244                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2245                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2246                            ))
2247
2248                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2249                    info.extend([
2250                        "\n## Portfolio distribution by companies\n"
2251                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2252                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2253                    ])
2254
2255                    for company in view["analytics"]["distrByCompanies"].keys():
2256                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2257                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2258                            info.append("| {} | {:<7} | {:<18} |\n".format(
2259                                "{}{}{}".format(
2260                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2261                                    company,
2262                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2263                                ),
2264                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2265                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2266                            ))
2267
2268                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2269                    info.extend([
2270                        "\n## Portfolio distribution by sectors\n"
2271                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2272                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2273                    ])
2274
2275                    for sector in view["analytics"]["distrBySectors"].keys():
2276                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2277                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2278                                sector,
2279                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2280                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2281                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2282                            ))
2283
2284                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2285                    info.extend([
2286                        "\n## Portfolio distribution by currencies\n"
2287                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2288                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2289                    ])
2290
2291                    for curr in view["analytics"]["distrByCurrencies"].keys():
2292                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2293                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2294                            info.append("| {} | {:<7} | {:<18} |\n".format(
2295                                "[{}] {}{}".format(
2296                                    curr,
2297                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2298                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2299                                ),
2300                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2301                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2302                            ))
2303
2304                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2305                    info.extend([
2306                        "\n## Portfolio distribution by countries\n"
2307                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2308                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2309                    ])
2310
2311                    for country in view["analytics"]["distrByCountries"].keys():
2312                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2313                            nameLen = len(country)
2314                            info.append("| {} | {:<7} | {:<18} |\n".format(
2315                                "{}{}".format(
2316                                    country,
2317                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2318                                ),
2319                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2320                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2321                            ))
2322
2323            infoText = "".join(info)
2324
2325            uLogger.info(infoText)
2326
2327            if details == "full" and self.overviewFile:
2328                filename = self.overviewFile
2329
2330            elif details == "digest" and self.overviewDigestFile:
2331                filename = self.overviewDigestFile
2332
2333            elif details == "positions" and self.overviewPositionsFile:
2334                filename = self.overviewPositionsFile
2335
2336            elif details == "orders" and self.overviewOrdersFile:
2337                filename = self.overviewOrdersFile
2338
2339            elif details == "analytics" and self.overviewAnalyticsFile:
2340                filename = self.overviewAnalyticsFile
2341
2342            else:
2343                filename = ""
2344
2345            if filename:
2346                with open(filename, "w", encoding="UTF-8") as fH:
2347                    fH.write(infoText)
2348
2349                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2350
2351        return view
2352
2353    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2354        """
2355        Returns history operations between two given dates for current `accountId`.
2356        If `reportFile` string is not empty then also save human-readable report.
2357        Shows some statistical data of closed positions.
2358
2359        :param start: see docstring in `GetDatesAsString()` method
2360        :param end: see docstring in `GetDatesAsString()` method
2361        :param show: if `True` then also prints all records to the console.
2362        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2363        :return: original list of dictionaries with history of deals records from API ("operations" key):
2364                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2365                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2366        """
2367        if self.accountId is None or not self.accountId:
2368            uLogger.error("Variable `accountId` must be defined for using this method!")
2369            raise Exception("Account ID required")
2370
2371        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2372
2373        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2374
2375        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2376        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2377        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2378        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2379        customStat = {}  # custom statistics in additional to responseJSON
2380
2381        # --- output report in human-readable format:
2382        if show or self.reportFile:
2383            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2384            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2385            nextDay = ""
2386
2387            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2388
2389            if len(ops) > 0:
2390                customStat = {
2391                    "opsCount": 0,  # total operations count
2392                    "buyCount": 0,  # buy operations
2393                    "sellCount": 0,  # sell operations
2394                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2395                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2396                    "payIn": {"rub": 0.},  # Deposit brokerage account
2397                    "payOut": {"rub": 0.},  # Withdrawals
2398                    "divs": {"rub": 0.},  # Dividends income
2399                    "coupons": {"rub": 0.},  # Coupon's income
2400                    "brokerCom": {"rub": 0.},  # Service commissions
2401                    "serviceCom": {"rub": 0.},  # Service commissions
2402                    "marginCom": {"rub": 0.},  # Margin commissions
2403                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2404                }
2405
2406                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2407                for item in ops:
2408                    if item["state"] == "OPERATION_STATE_EXECUTED":
2409                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2410
2411                        # count buy operations:
2412                        if "_BUY" in item["operationType"]:
2413                            customStat["buyCount"] += 1
2414
2415                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2416                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2417
2418                            else:
2419                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2420
2421                        # count sell operations:
2422                        elif "_SELL" in item["operationType"]:
2423                            customStat["sellCount"] += 1
2424
2425                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2426                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2427
2428                            else:
2429                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2430
2431                        # count incoming operations:
2432                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2433                            if item["payment"]["currency"] in customStat["payIn"].keys():
2434                                customStat["payIn"][item["payment"]["currency"]] += payment
2435
2436                            else:
2437                                customStat["payIn"][item["payment"]["currency"]] = payment
2438
2439                        # count withdrawals operations:
2440                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2441                            if item["payment"]["currency"] in customStat["payOut"].keys():
2442                                customStat["payOut"][item["payment"]["currency"]] += payment
2443
2444                            else:
2445                                customStat["payOut"][item["payment"]["currency"]] = payment
2446
2447                        # count dividends income:
2448                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2449                            if item["payment"]["currency"] in customStat["divs"].keys():
2450                                customStat["divs"][item["payment"]["currency"]] += payment
2451
2452                            else:
2453                                customStat["divs"][item["payment"]["currency"]] = payment
2454
2455                        # count coupon's income:
2456                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2457                            if item["payment"]["currency"] in customStat["coupons"].keys():
2458                                customStat["coupons"][item["payment"]["currency"]] += payment
2459
2460                            else:
2461                                customStat["coupons"][item["payment"]["currency"]] = payment
2462
2463                        # count broker commissions:
2464                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2465                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2466                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2467
2468                            else:
2469                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2470
2471                        # count service commissions:
2472                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2473                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2474                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2475
2476                            else:
2477                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2478
2479                        # count margin commissions:
2480                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2481                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2482                                customStat["marginCom"][item["payment"]["currency"]] += payment
2483
2484                            else:
2485                                customStat["marginCom"][item["payment"]["currency"]] = payment
2486
2487                        # count withholding taxes:
2488                        elif "_TAX" in item["operationType"]:
2489                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2490                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2491
2492                            else:
2493                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2494
2495                        else:
2496                            continue
2497
2498                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2499
2500                # --- view "Actions" lines:
2501                info.extend([
2502                    "| 1                          | 2                             | 3                            | 4                    | 5                      |\n",
2503                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2504                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2505                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2506                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2507                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2508                    ),
2509                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2510                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2511                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2512                    ),
2513                ])
2514
2515                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2516                for key in opsKeys:
2517                    if key == "rub":
2518                        continue
2519
2520                    info.extend([
2521                        "|                            |                               | {:<28} |                      |                        |\n".format(
2522                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2523                        ),
2524                        "|                            |                               | {:<28} |                      |                        |\n".format(
2525                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2526                        ),
2527                    ])
2528
2529                info.append(splitLine1)
2530
2531                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2532                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2533                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2534                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2535                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2536                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2537                    )
2538
2539                # --- view "Payments" lines:
2540                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2541                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2542
2543                for key in paymentsKeys:
2544                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2545
2546                info.append(splitLine1)
2547
2548                # --- view "Commissions and taxes" lines:
2549                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2550                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2551
2552                for key in comKeys:
2553                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2554
2555                info.append(splitLine1)
2556
2557                info.extend([
2558                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2559                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2560                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2561                ])
2562
2563            else:
2564                info.append("Broker returned no operations during this period\n")
2565
2566            # --- view "Operations" section:
2567            for item in ops:
2568                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2569                    continue
2570
2571                else:
2572                    self.figi = item["figi"] if item["figi"] else ""
2573                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2574                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2575
2576                    # group of deals during one day:
2577                    if nextDay and item["date"].split("T")[0] != nextDay:
2578                        info.append(splitLine2)
2579                        nextDay = ""
2580
2581                    else:
2582                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2583
2584                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2585                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2586                        self.figi if self.figi else "—",
2587                        instrument["ticker"] if instrument else "—",
2588                        instrument["type"] if instrument else "—",
2589                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2590                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2591                        TKS_OPERATION_STATES[item["state"]],
2592                        TKS_OPERATION_TYPES[item["operationType"]],
2593                    ))
2594
2595            infoText = "".join(info)
2596
2597            if show:
2598                uLogger.info(infoText)
2599
2600            if self.reportFile:
2601                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2602                    fH.write(infoText)
2603
2604                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2605
2606        return ops, customStat
2607
2608    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2609        """
2610        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2611
2612        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2613        Warning! Broker server used ISO UTC time by default.
2614
2615        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2616        Also, `historyFile` used to update history with `onlyMissing` parameter.
2617
2618        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2619
2620        :param start: see docstring in `GetDatesAsString()` method.
2621        :param end: see docstring in `GetDatesAsString()` method.
2622        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2623                         `"hour"`, `"day"`. Default: `"hour"`.
2624        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2625                            False by default. Warning! History appends only from last candle to current time
2626                            with always update last candle!
2627        :param csvSep: separator if csv-file is used, `,` by default.
2628        :param show: if `True` then also prints Pandas DataFrame to the console.
2629        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2630                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2631        """
2632        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2633        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2634        history = None  # empty pandas object for history
2635
2636        if interval not in TKS_CANDLE_INTERVALS.keys():
2637            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2638            raise Exception("Incorrect value")
2639
2640        if not (self.ticker or self.figi):
2641            uLogger.error("Ticker or FIGI must be defined!")
2642            raise Exception("Ticker or FIGI required")
2643
2644        if self.ticker and not self.figi:
2645            instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False)
2646            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2647
2648        if self.figi and not self.ticker:
2649            instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False)
2650            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2651
2652        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2653        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2654        if interval.lower() != "day":
2655            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2656
2657        delta = dtEnd - dtStart  # current UTC time minus last time in file
2658        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2659
2660        # calculate history length in candles:
2661        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2662        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2663            length += 1  # to avoid fraction time
2664
2665        # calculate data blocks count:
2666        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2667
2668        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2669        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2670        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2671        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2672        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2673
2674        tempOld = None  # pandas object for old history, if --only-missing key present
2675        lastTime = None  # datetime object of last old candle in file
2676
2677        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2678            uLogger.debug("--only-missing key present, add only last missing candles...")
2679            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2680
2681            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2682
2683            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2684            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2685            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2686            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2687
2688            # get last datetime object from last string in file or minus 1 delta if file is empty:
2689            if len(tempOld) > 0:
2690                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2691
2692            else:
2693                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2694
2695            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2696
2697        responseJSONs = []  # raw history blocks of data
2698
2699        blockEnd = dtEnd
2700        for item in range(blocks):
2701            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2702            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2703
2704            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2705                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2706            ))
2707
2708            if blockStart == blockEnd:
2709                uLogger.debug("Skipped this zero-length block...")
2710
2711            else:
2712                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2713                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2714                self.body = str({
2715                    "figi": self.figi,
2716                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2717                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2718                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2719                })
2720                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False)
2721
2722                if "code" in responseJSON.keys():
2723                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2724
2725                else:
2726                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2727                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2728
2729                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2730
2731            blockEnd = blockStart
2732
2733        printCount = len(responseJSONs)  # candles to show in console
2734        if responseJSONs:
2735            tempHistory = pd.DataFrame(
2736                data={
2737                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2738                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2739                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2740                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2741                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2742                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2743                    "volume": [int(item["volume"]) for item in responseJSONs],
2744                },
2745                index=range(len(responseJSONs)),
2746                columns=["date", "time", "open", "high", "low", "close", "volume"],
2747            )
2748            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2749            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2750
2751            # append only newest candles to old history if --only-missing key present:
2752            if onlyMissing and tempOld is not None and lastTime is not None:
2753                index = 0  # find start index in tempHistory data:
2754
2755                for i, item in tempHistory.iterrows():
2756                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2757
2758                    if curTime == lastTime:
2759                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2760                        index = i
2761                        printCount = index + 1
2762                        break
2763
2764                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2765
2766            else:
2767                history = tempHistory  # if no `--only-missing` key then load full data from server
2768
2769            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2770
2771        if history is not None and not history.empty:
2772            if show:
2773                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2774                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2775                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2776                ))
2777
2778        else:
2779            uLogger.warning("Received an empty candles history!")
2780
2781        if self.historyFile is not None:
2782            if history is not None and not history.empty:
2783                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2784                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2785
2786            else:
2787                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2788
2789        else:
2790            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2791
2792        return history
2793
2794    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2795        """
2796        Load candles history from csv-file and return Pandas DataFrame object.
2797
2798        See also: `History()` and `ShowHistoryChart()` methods.
2799
2800        :param filePath: path to csv-file to open.
2801        """
2802        loadedHistory = None  # init candles data object
2803
2804        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2805
2806        if os.path.exists(filePath):
2807            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2808
2809            tfStr = self.priceModel.FormattedDelta(
2810                self.priceModel.timeframe,
2811                "{days} days {hours}h {minutes}m {seconds}s",
2812            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2813                self.priceModel.timeframe,
2814                "{hours}h {minutes}m {seconds}s",
2815            )
2816
2817            if loadedHistory is not None and not loadedHistory.empty:
2818                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2819                    len(loadedHistory),
2820                    tfStr,
2821                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2822                )
2823
2824            else:
2825                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2826
2827        else:
2828            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2829
2830        return loadedHistory
2831
2832    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2833        """
2834        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2835
2836        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2837        Default: `index.html` (both for interact and non-interact candlesticks chart).
2838
2839        See also: `History()` and `LoadHistory()` methods.
2840
2841        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2842        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2843                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2844                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2845                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2846        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2847                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2848        """
2849        if isinstance(candles, str):
2850            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2851            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2852
2853        elif isinstance(candles, pd.DataFrame):
2854            self.priceModel.prices = candles  # set candles chain from variable
2855            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2856
2857            if "datetime" not in candles.columns:
2858                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2859
2860        else:
2861            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2862            raise Exception("Incorrect value")
2863
2864        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2865
2866        if interact:
2867            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2868
2869            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2870
2871        else:
2872            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2873
2874            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2875
2876        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2877
2878    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2879        """
2880        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2881        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2882
2883        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2884
2885        :param operation: string "Buy" or "Sell".
2886        :param lots: volume, integer count of lots >= 1.
2887        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2888        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2889        :param expDate: string "Undefined" by default or local date in future,
2890                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2891        :return: JSON with response from broker server.
2892        """
2893        if self.accountId is None or not self.accountId:
2894            uLogger.error("Variable `accountId` must be defined for using this method!")
2895            raise Exception("Account ID required")
2896
2897        if operation is None or not operation or operation not in ("Buy", "Sell"):
2898            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2899            raise Exception("Incorrect value")
2900
2901        if lots is None or lots < 1:
2902            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2903            lots = 1
2904
2905        if tp is None or tp < 0:
2906            tp = 0
2907
2908        if sl is None or sl < 0:
2909            sl = 0
2910
2911        if expDate is None or not expDate:
2912            expDate = "Undefined"
2913
2914        if not (self.ticker or self.figi):
2915            uLogger.error("Ticker or FIGI must be defined!")
2916            raise Exception("Ticker or FIGI required")
2917
2918        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
2919        self.ticker = instrument["ticker"]
2920        self.figi = instrument["figi"]
2921
2922        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2923
2924        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2925        self.body = str({
2926            "figi": self.figi,
2927            "quantity": str(lots),
2928            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2929            "accountId": str(self.accountId),
2930            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2931        })
2932        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False)
2933
2934        if "orderId" in response.keys():
2935            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2936                operation, response["orderId"],
2937                self.ticker, self.figi, lots,
2938                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2939                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2940                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2941            ))
2942
2943        else:
2944            uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.")
2945
2946        if tp > 0:
2947            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2948
2949        if sl > 0:
2950            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2951
2952        return response
2953
2954    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2955        """
2956        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2957        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2958
2959        See also: `Order()` and `Trade()` docstrings.
2960
2961        :param lots: volume, integer count of lots >= 1.
2962        :param tp: float > 0, take profit price of stop-order.
2963        :param sl: float > 0, stop loss price of stop-order.
2964        :param expDate: it's a local date in future.
2965                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2966        :return: JSON with response from broker server.
2967        """
2968        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2969
2970    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2971        """
2972        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2973        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2974
2975        See also: `Order()` and `Trade()` docstrings.
2976
2977        :param lots: volume, integer count of lots >= 1.
2978        :param tp: float > 0, take profit price of stop-order.
2979        :param sl: float > 0, stop loss price of stop-order.
2980        :param expDate: it's a local date in the future.
2981                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2982        :return: JSON with response from broker server.
2983        """
2984        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2985
2986    def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2987        """
2988        Close position of given instruments.
2989
2990        :param tickers: tickers list of instruments that must be closed.
2991        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2992                         This avoids unnecessary downloading data from the server.
2993        """
2994        if not tickers:
2995            uLogger.info("Tickers list is empty, nothing to close.")
2996
2997        else:
2998            if portfolio is None or not portfolio:
2999                portfolio = self.Overview(show=False)
3000
3001            allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3002            uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers))
3003
3004            for ticker in tickers:
3005                if ticker not in allOpenedTickers:
3006                    uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker))
3007                    continue
3008
3009                # search open trade info about instrument by ticker:
3010                instrument = {}
3011                for iType in TKS_INSTRUMENTS:
3012                    if instrument:
3013                        break
3014
3015                    for item in portfolio["stat"][iType]:
3016                        if item["ticker"] == ticker:
3017                            instrument = item
3018                            break
3019
3020                if instrument:
3021                    self.ticker = ticker
3022                    self.figi = instrument["figi"]
3023
3024                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3025                        self.ticker,
3026                        self.figi,
3027                        int(instrument["volume"]),
3028                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3029                    ))
3030
3031                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3032
3033                    if tradeLots > 0:
3034                        if instrument["blocked"] > 0:
3035                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3036                                instrument["blocked"],
3037                                self.ticker,
3038                                tradeLots,
3039                            ))
3040
3041                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3042                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3043
3044                    else:
3045                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3046
3047    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3048        """
3049        Close all positions of given instruments with defined type.
3050
3051        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3052        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3053                         This avoids unnecessary downloading data from the server.
3054        """
3055        if iType not in TKS_INSTRUMENTS:
3056            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3057
3058        else:
3059            if portfolio is None or not portfolio:
3060                portfolio = self.Overview(show=False)
3061
3062            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3063            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3064
3065            if tickers and portfolio:
3066                self.CloseTrades(tickers, portfolio)
3067
3068            else:
3069                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3070
3071    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3072        """
3073        Universal method to create market or limit orders with all available parameters for current `accountId`.
3074        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3075
3076        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3077        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3078
3079        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3080        then broker immediately open market order as you can do simple --buy or --sell operations!
3081
3082        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3083        When current price will go up or down to target price value then broker opens a limit order.
3084        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3085
3086        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3087
3088        :param operation: string "Buy" or "Sell".
3089        :param orderType: string "Limit" or "Stop".
3090        :param lots: volume, integer count of lots >= 1.
3091        :param targetPrice: target price > 0. This is open trade price for limit order.
3092        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3093                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3094        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3095                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3096                         Stop loss order always executed by market price.
3097        :param expDate: string "Undefined" by default or local date in future.
3098                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3099                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3100                        A limit order has no expiration date, it lasts until the end of the trading day.
3101        :return: JSON with response from broker server.
3102        """
3103        if self.accountId is None or not self.accountId:
3104            uLogger.error("Variable `accountId` must be defined for using this method!")
3105            raise Exception("Account ID required")
3106
3107        if operation is None or not operation or operation not in ("Buy", "Sell"):
3108            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3109            raise Exception("Incorrect value")
3110
3111        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3112            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3113            raise Exception("Incorrect value")
3114
3115        if lots is None or lots < 1:
3116            uLogger.error("You must define trade volume > 0: integer count of lots!")
3117            raise Exception("Incorrect value")
3118
3119        if targetPrice is None or targetPrice <= 0:
3120            uLogger.error("Target price for limit-order must be greater than 0!")
3121            raise Exception("Incorrect value")
3122
3123        if limitPrice is None or limitPrice <= 0:
3124            limitPrice = targetPrice
3125
3126        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3127            stopType = "Limit"
3128
3129        if expDate is None or not expDate:
3130            expDate = "Undefined"
3131
3132        if not (self.ticker or self.figi):
3133            uLogger.error("Tocker or FIGI must be defined!")
3134            raise Exception("Ticker or FIGI required")
3135
3136        response = {}
3137        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
3138        self.ticker = instrument["ticker"]
3139        self.figi = instrument["figi"]
3140
3141        if orderType == "Limit":
3142            uLogger.debug(
3143                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3144                    self.ticker, self.figi,
3145                    operation, lots, targetPrice, instrument["currency"],
3146                ))
3147
3148            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3149            self.body = str({
3150                "figi": self.figi,
3151                "quantity": str(lots),
3152                "price": FloatToNano(targetPrice),
3153                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3154                "accountId": str(self.accountId),
3155                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3156            })
3157            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3158
3159            if "orderId" in response.keys():
3160                uLogger.info(
3161                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3162                        response["orderId"],
3163                        self.ticker, self.figi,
3164                        operation, lots, targetPrice, instrument["currency"],
3165                    ))
3166
3167                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3168                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3169                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3170                            targetPrice, instrument["currency"],
3171                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3172                        ))
3173
3174                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3175                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3176                            targetPrice, instrument["currency"],
3177                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3178                        ))
3179
3180            else:
3181                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3182
3183        if orderType == "Stop":
3184            uLogger.debug(
3185                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3186                    self.ticker, self.figi,
3187                    operation, lots,
3188                    targetPrice, instrument["currency"],
3189                    limitPrice, instrument["currency"],
3190                    stopType, expDate,
3191                ))
3192
3193            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3194            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3195            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3196
3197            body = {
3198                "figi": self.figi,
3199                "quantity": str(lots),
3200                "price": FloatToNano(limitPrice),
3201                "stopPrice": FloatToNano(targetPrice),
3202                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3203                "accountId": str(self.accountId),
3204                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3205                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3206            }
3207
3208            if expDateUTC:
3209                body["expireDate"] = expDateUTC
3210
3211            self.body = str(body)
3212            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3213
3214            if "stopOrderId" in response.keys():
3215                uLogger.info(
3216                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3217                        response["stopOrderId"],
3218                        self.ticker, self.figi,
3219                        operation, lots,
3220                        targetPrice, instrument["currency"],
3221                        limitPrice, instrument["currency"],
3222                        TKS_STOP_ORDER_TYPES[stopOrderType],
3223                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3224                    ))
3225
3226                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3227                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3228                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3229                            targetPrice, instrument["currency"],
3230                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3231                        ))
3232
3233                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3234                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3235                            targetPrice, instrument["currency"],
3236                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3237                        ))
3238
3239            else:
3240                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3241
3242        return response
3243
3244    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3245        """
3246        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3247        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3248        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3249        See also: `Order()` docstring.
3250
3251        :param lots: volume, integer count of lots >= 1.
3252        :param targetPrice: target price > 0. This is open trade price for limit order.
3253        :return: JSON with response from broker server.
3254        """
3255        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3256
3257    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3258        """
3259        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3260        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3261        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3262        target price value then broker opens a limit order. See also: `Order()` docstring.
3263
3264        :param lots: volume, integer count of lots >= 1.
3265        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3266        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3267                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3268        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3269                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3270        :param expDate: string "Undefined" by default or local date in future.
3271                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3272                        This date is converting to UTC format for server.
3273        :return: JSON with response from broker server.
3274        """
3275        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3276
3277    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3278        """
3279        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3280        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3281        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3282        See also: `Order()` docstring.
3283
3284        :param lots: volume, integer count of lots >= 1.
3285        :param targetPrice: target price > 0. This is open trade price for limit order.
3286        :return: JSON with response from broker server.
3287        """
3288        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3289
3290    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3291        """
3292        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3293        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3294        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3295        target price value then broker opens a limit order. See also: `Order()` docstring.
3296
3297        :param lots: volume, integer count of lots >= 1.
3298        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3299        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3300                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3301        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3302                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3303        :param expDate: string "Undefined" by default or local date in future.
3304                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3305                        This date is converting to UTC format for server.
3306        :return: JSON with response from broker server.
3307        """
3308        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3309
3310    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3311        """
3312        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3313
3314        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3315        :param allOrdersIDs: pre-received lists of all active pending orders.
3316                             This avoids unnecessary downloading data from the server.
3317        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3318        """
3319        if self.accountId is None or not self.accountId:
3320            uLogger.error("Variable `accountId` must be defined for using this method!")
3321            raise Exception("Account ID required")
3322
3323        if orderIDs:
3324            if allOrdersIDs is None or not allOrdersIDs:
3325                rawOrders = self.RequestPendingOrders()
3326                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3327
3328            if allStopOrdersIDs is None or not allStopOrdersIDs:
3329                rawStopOrders = self.RequestStopOrders()
3330                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3331
3332            for orderID in orderIDs:
3333                idInPendingOrders = orderID in allOrdersIDs
3334                idInStopOrders = orderID in allStopOrdersIDs
3335
3336                if not (idInPendingOrders or idInStopOrders):
3337                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3338                    continue
3339
3340                else:
3341                    if idInPendingOrders:
3342                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3343
3344                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3345                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3346                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3347                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3348
3349                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3350                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3351                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3352
3353                        else:
3354                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3355
3356                    elif idInStopOrders:
3357                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3358
3359                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3360                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3361                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3362                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3363
3364                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3365                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3366                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3367
3368                        else:
3369                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3370
3371                    else:
3372                        continue
3373
3374    def CloseAllOrders(self) -> None:
3375        """
3376        Gets a list of open pending and stop orders and cancel it all.
3377        """
3378        rawOrders = self.RequestPendingOrders()
3379        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3380        lenOrders = len(allOrdersIDs)
3381
3382        rawStopOrders = self.RequestStopOrders()
3383        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3384        lenSOrders = len(allStopOrdersIDs)
3385
3386        if lenOrders > 0 or lenSOrders > 0:
3387            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3388
3389            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3390
3391        else:
3392            uLogger.info("Orders not found, nothing to cancel.")
3393
3394    def CloseAll(self, *args) -> None:
3395        """
3396        Close all available (not blocked) opened trades and orders.
3397
3398        Also, you can select one or more keywords case-insensitive:
3399        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3400
3401        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3402        """
3403        overview = self.Overview(show=False)  # get all open trades info
3404
3405        if len(args) == 0:
3406            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3407            self.CloseAllOrders()  # close all pending and stop orders
3408
3409            for iType in TKS_INSTRUMENTS:
3410                if iType != "Currencies":
3411                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3412
3413        else:
3414            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3415            lowerArgs = [x.lower() for x in args]
3416
3417            if "orders" in lowerArgs:
3418                self.CloseAllOrders()  # close all pending and stop orders
3419
3420            for iType in TKS_INSTRUMENTS:
3421                if iType.lower() in lowerArgs and iType != "Currencies":
3422                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3423
3424    @staticmethod
3425    def ParseOrderParameters(operation, **inputParameters):
3426        """
3427        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3428
3429        :param operation: string "Buy" or "Sell".
3430        :param inputParameters: this is dict of strings that looks like this
3431               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3432               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3433               "prices" key: one or more prices to open limit-orders
3434               Counts of values in lots and prices lists must be equals!
3435        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3436        """
3437        # TODO: update order grid work with api v2
3438        pass
3439        # uLogger.debug("Input parameters: {}".format(inputParameters))
3440        #
3441        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3442        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3443        #     raise Exception("Incorrect value")
3444        #
3445        # if "l" in inputParameters.keys():
3446        #     inputParameters["lots"] = inputParameters.pop("l")
3447        #
3448        # if "p" in inputParameters.keys():
3449        #     inputParameters["prices"] = inputParameters.pop("p")
3450        #
3451        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3452        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3453        #     raise Exception("Incorrect value")
3454        #
3455        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3456        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3457        #
3458        # if len(lots) != len(prices):
3459        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3460        #     raise Exception("Incorrect value")
3461        #
3462        # uLogger.debug("Extracted parameters for orders:")
3463        # uLogger.debug("lots = {}".format(lots))
3464        # uLogger.debug("prices = {}".format(prices))
3465        #
3466        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3467        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3468        # uLogger.debug("Order parameters: {}".format(result))
3469        #
3470        # return result
3471
3472    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3473        """
3474        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3475
3476        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3477        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3478        """
3479        result = False
3480        msg = "Instrument not defined!"
3481
3482        if portfolio is None or not portfolio:
3483            portfolio = self.Overview(show=False)
3484
3485        if self.ticker:
3486            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3487            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3488
3489            for iType in TKS_INSTRUMENTS:
3490                for instrument in portfolio["stat"][iType]:
3491                    if instrument["ticker"] == self.ticker:
3492                        result = True
3493                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3494                        break
3495
3496        elif self.figi:
3497            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3498            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3499
3500            for iType in TKS_INSTRUMENTS:
3501                for instrument in portfolio["stat"][iType]:
3502                    if instrument["figi"] == self.figi:
3503                        result = True
3504                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3505                        break
3506
3507        else:
3508            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3509
3510        uLogger.debug(msg)
3511
3512        return result
3513
3514    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3515        """
3516        Returns instrument is in the user's portfolio if it presents there.
3517        Instrument must be defined by `ticker` (highly priority) or `figi`.
3518
3519        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3520        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3521        """
3522        result = None
3523        msg = "Instrument not defined!"
3524
3525        if portfolio is None or not portfolio:
3526            portfolio = self.Overview(show=False)
3527
3528        if self.ticker:
3529            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3530            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3531
3532            for iType in TKS_INSTRUMENTS:
3533                for instrument in portfolio["stat"][iType]:
3534                    if instrument["ticker"] == self.ticker:
3535                        result = instrument
3536                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3537                        break
3538
3539        elif self.figi:
3540            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3541            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3542
3543            for iType in TKS_INSTRUMENTS:
3544                for instrument in portfolio["stat"][iType]:
3545                    if instrument["figi"] == self.figi:
3546                        result = instrument
3547                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3548                        break
3549
3550        else:
3551            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3552
3553        uLogger.debug(msg)
3554
3555        return result
3556
3557    def RequestLimits(self) -> dict:
3558        """
3559        Method for obtaining the available funds for withdrawal for current `accountId`.
3560
3561        See also:
3562        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3563        - `OverviewLimits()` method
3564
3565        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3566                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3567                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3568                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3569        """
3570        if self.accountId is None or not self.accountId:
3571            uLogger.error("Variable `accountId` must be defined for using this method!")
3572            raise Exception("Account ID required")
3573
3574        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3575
3576        self.body = str({"accountId": self.accountId})
3577        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3578        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3579
3580        uLogger.debug("Records about available funds for withdrawal successfully received")
3581
3582        return rawLimits
3583
3584    def OverviewLimits(self, show: bool = False) -> dict:
3585        """
3586        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3587
3588        See also: `RequestLimits()`.
3589
3590        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3591        :return: dict with raw parsed data from server and some calculated statistics about it.
3592        """
3593        if self.accountId is None or not self.accountId:
3594            uLogger.error("Variable `accountId` must be defined for using this method!")
3595            raise Exception("Account ID required")
3596
3597        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3598
3599        view = {
3600            "rawLimits": rawLimits,
3601            "limits": {  # parsed data for every currency:
3602                "money": {  # this is an array of portfolio currency positions
3603                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3604                },
3605                "blocked": {  # this is an array of blocked currency
3606                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3607                },
3608                "blockedGuarantee": {  # this is locked money under collateral for futures
3609                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3610                },
3611            },
3612        }
3613
3614        # --- Prepare text table with limits in human-readable format:
3615        if show:
3616            info = [
3617                "# Withdrawal limits\n\n",
3618                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3619                "* **Account ID:** [{}]\n".format(self.accountId),
3620                "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3621                "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3622            ]
3623
3624            for curr in view["limits"]["money"].keys():
3625                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3626                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3627                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3628
3629                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3630                    "[{}]".format(curr),
3631                    "{:.2f}".format(view["limits"]["money"][curr]),
3632                    "{:.2f}".format(availableMoney),
3633                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3634                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3635                )
3636
3637                if curr == "rub":
3638                    info.insert(5, infoStr)  # insert at first position in table and after headers
3639
3640                else:
3641                    info.append(infoStr)
3642
3643            infoText = "".join(info)
3644
3645            uLogger.info(infoText)
3646
3647            if self.withdrawalLimitsFile:
3648                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3649                    fH.write(infoText)
3650
3651                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3652
3653        return view
3654
3655    def RequestAccounts(self) -> dict:
3656        """
3657        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3658
3659        See also:
3660        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3661        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3662        - `OverviewUserInfo()` method
3663
3664        :return: dict with raw data from server that contains accounts info. Example of dict:
3665                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3666                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3667                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3668                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3669        """
3670        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3671
3672        self.body = str({})
3673        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3674        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3675
3676        uLogger.debug("Records about available accounts successfully received")
3677
3678        return rawAccounts
3679
3680    def RequestUserInfo(self) -> dict:
3681        """
3682        Method for requesting common user's information.
3683
3684        See also:
3685        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3686        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3687        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3688        - `OverviewUserInfo()` method
3689
3690        :return: dict with raw data from server that contains user's information. Example of dict:
3691                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3692                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3693        """
3694        uLogger.debug("Requesting common user's information. Wait, please...")
3695
3696        self.body = str({})
3697        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3698        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3699
3700        uLogger.debug("Records about current user successfully received")
3701
3702        return rawUserInfo
3703
3704    def RequestMarginStatus(self, accountId: str = None) -> dict:
3705        """
3706        Method for requesting margin calculation for defined account ID.
3707
3708        See also:
3709        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3710        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3711        - `OverviewUserInfo()` method
3712
3713        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3714        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3715                 Example of responses:
3716                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3717                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3718                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3719                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3720                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3721                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3722        """
3723        if accountId is None or not accountId:
3724            if self.accountId is None or not self.accountId:
3725                uLogger.error("Variable `accountId` must be defined for using this method!")
3726                raise Exception("Account ID required")
3727
3728            else:
3729                accountId = self.accountId  # use `self.accountId` (main ID) by default
3730
3731        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3732
3733        self.body = str({"accountId": accountId})
3734        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3735        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3736
3737        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3738            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3739            rawMargin = {}
3740
3741        else:
3742            uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3743
3744        return rawMargin
3745
3746    def RequestTariffLimits(self) -> dict:
3747        """
3748        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3749
3750        See also:
3751        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3752        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3753        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3754        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3755        - `OverviewUserInfo()` method
3756
3757        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3758                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3759                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3760        """
3761        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3762
3763        self.body = str({})
3764        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3765        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3766
3767        uLogger.debug("Records with limits of current tariff successfully received")
3768
3769        return rawTariffLimits
3770
3771    def RequestBondCoupons(self, iJSON: dict) -> dict:
3772        """
3773        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3774        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3775        All dates are in UTC timezone.
3776
3777        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3778        Documentation:
3779        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3780        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3781
3782        See also: `ExtendBondsData()`.
3783
3784        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3785                      If raw iJSON is not data of bond then server returns an error [400] with message:
3786                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3787        :return: dictionary with bond payment calendar. Response example
3788                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3789                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3790                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3791                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3792        """
3793        if iJSON["figi"] is None or not iJSON["figi"]:
3794            uLogger.error("FIGI must be defined for using this method!")
3795            raise Exception("FIGI required")
3796
3797        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3798        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3799
3800        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3801            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3802            self.figi,
3803            startDate,
3804            endDate,
3805        ))
3806
3807        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3808        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3809        calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False)
3810
3811        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3812            uLogger.warning("Instrument type is not bond!")
3813
3814        else:
3815            uLogger.debug("Records about bond payment calendar successfully received")
3816
3817        return calendar
3818
3819    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3820        """
3821        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3822        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3823        coupon yields, current yields and some statistics etc.
3824
3825        WARNING! This is too long operation if a lot of bonds requested from broker server.
3826
3827        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3828
3829        :param instruments: list of strings with tickers or FIGIs.
3830        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3831                     for further used by data scientists or stock analytics.
3832        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3833                 In XLSX-file and Pandas DataFrame fields mean:
3834                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3835                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3836        """
3837        if instruments is None or not instruments:
3838            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3839            raise Exception("Ticker or FIGI required")
3840
3841        if isinstance(instruments, str):
3842            instruments = [instruments]
3843
3844        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3845
3846        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3847
3848        iCount = len(uniqueInstruments)
3849        tooLong = iCount >= 20
3850        if tooLong:
3851            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3852
3853        bonds = None
3854        for i, self.figi in enumerate(uniqueInstruments):
3855            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3856
3857            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3858                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3859                rawBond = self.SearchByFIGI(requestPrice=True)
3860
3861                # Widen raw data with UTC current time (iData["actualDateTime"]):
3862                actualDate = datetime.now(tzutc())
3863                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3864
3865                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3866                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3867
3868                # Replace some values with human-readable:
3869                iData["nominalCurrency"] = iData["nominal"]["currency"]
3870                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3871                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3872                iData["aciCurrency"] = iData["aciValue"]["currency"]
3873                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3874                iData["issueSize"] = int(iData["issueSize"])
3875                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3876                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3877                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3878                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3879                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3880                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3881                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3882                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3883                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3884                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3885
3886                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3887                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3888                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3889                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3890                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3891                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3892                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3893                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3894                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3895                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3896                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3897
3898                # Widen raw data with calendar data from `rawCalendar` values:
3899                calendarData = []
3900                for item in iData["rawCalendar"]["events"]:
3901                    calendarData.append({
3902                        "couponDate": item["couponDate"],
3903                        "couponNumber": int(item["couponNumber"]),
3904                        "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3905                        "payCurrency": item["payOneBond"]["currency"],
3906                        "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3907                        "couponType": TKS_COUPON_TYPES[item["couponType"]],
3908                        "couponStartDate": item["couponStartDate"],
3909                        "couponEndDate": item["couponEndDate"],
3910                        "couponPeriod": item["couponPeriod"],
3911                    })
3912
3913                # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3914                if "maturityDate" not in iData.keys():
3915                    iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3916
3917                # Widen raw data with Coupon Rate.
3918                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3919                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3920                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3921                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3922
3923                # Widen raw data with Yield to Maturity (YTM) on current date.
3924                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3925                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3926                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3927                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3928                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3929                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3930
3931                iData["calendar"] = calendarData  # adds calendar at the end
3932
3933                # Remove not used data:
3934                iData.pop("uid")
3935                iData.pop("positionUid")
3936                iData.pop("currentPrice")
3937                iData.pop("rawCalendar")
3938
3939                colNames = list(iData.keys())
3940                if bonds is None:
3941                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3942
3943                else:
3944                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3945
3946            else:
3947                uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"]))
3948
3949            processed = round(100 * (i + 1) / iCount, 1)
3950            if tooLong and processed % 5 == 0:
3951                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3952
3953            else:
3954                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3955
3956        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3957
3958        # Saving bonds from Pandas DataFrame to XLSX sheet:
3959        if xlsx and self.bondsXLSXFile:
3960            with pd.ExcelWriter(
3961                    path=self.bondsXLSXFile,
3962                    date_format=TKS_DATE_FORMAT,
3963                    datetime_format=TKS_DATE_TIME_FORMAT,
3964                    mode="w",
3965            ) as writer:
3966                bonds.to_excel(
3967                    writer,
3968                    sheet_name="Extended bonds data",
3969                    index=True,
3970                    encoding="UTF-8",
3971                    freeze_panes=(1, 1),
3972                )  # saving as XLSX-file with freeze first row and column as headers
3973
3974            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3975
3976        return bonds
3977
3978    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3979        """
3980        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
3981
3982        WARNING! This is too long operation if a lot of bonds requested from broker server.
3983
3984        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3985
3986        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
3987                        extended information about bonds: main info, current prices, bond payment calendar,
3988                        coupon yields, current yields and some statistics etc.
3989                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3990        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
3991                     for further used by data scientists or stock analytics.
3992        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
3993        """
3994        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3995            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3996
3997        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3998
3999        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4000        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4001        calendar = None
4002        for bond in extBonds.iterrows():
4003            for item in bond[1]["calendar"]:
4004                cData = {
4005                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4006                    "couponDate": item["couponDate"],
4007                    "figi": bond[1]["figi"],
4008                    "ticker": bond[1]["ticker"],
4009                    "name": bond[1]["name"],
4010                    "couponNumber": item["couponNumber"],
4011                    "payOneBond": item["payOneBond"],
4012                    "payCurrency": item["payCurrency"],
4013                    "couponType": item["couponType"],
4014                    "couponPeriod": item["couponPeriod"],
4015                    "fixDate": item["fixDate"],
4016                    "couponStartDate": item["couponStartDate"],
4017                    "couponEndDate": item["couponEndDate"],
4018                }
4019
4020                if calendar is None:
4021                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4022
4023                else:
4024                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4025
4026        calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4027
4028        # Saving calendar from Pandas DataFrame to XLSX sheet:
4029        if xlsx:
4030            xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4031
4032            with pd.ExcelWriter(
4033                    path=xlsxCalendarFile,
4034                    date_format=TKS_DATE_FORMAT,
4035                    datetime_format=TKS_DATE_TIME_FORMAT,
4036                    mode="w",
4037            ) as writer:
4038                humanReadable = calendar.copy(deep=True)
4039                humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4040                humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4041                humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4042                humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4043                humanReadable.columns = colNames  # human-readable column names
4044
4045                humanReadable.to_excel(
4046                    writer,
4047                    sheet_name="Bond payments calendar",
4048                    index=False,
4049                    encoding="UTF-8",
4050                    freeze_panes=(1, 2),
4051                )  # saving as XLSX-file with freeze first row and column as headers
4052
4053                del humanReadable  # release df in memory
4054
4055            uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4056
4057        return calendar
4058
4059    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4060        """
4061        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4062        Also, creates Markdown file with calendar data, `calendar.md` by default.
4063
4064        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4065
4066        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4067                        extended information about bonds: main info, current prices, bond payment calendar,
4068                        coupon yields, current yields and some statistics etc.
4069                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4070        :param show: if `True` then also printing bonds payment calendar to the console,
4071                     otherwise save to file `calendarFile` only. `False` by default.
4072        :return: multilines text in Markdown format with bonds payment calendar as a table.
4073        """
4074        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4075            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4076
4077        infoText = "# Bond payments calendar\n\n"
4078
4079        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4080
4081        if not calendar.empty:
4082            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4083
4084            info = [
4085                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4086                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4087            ]
4088
4089            newMonth = False
4090            notOneBond = calendar["figi"].nunique() > 1
4091            for i, bond in enumerate(calendar.iterrows()):
4092                if newMonth and notOneBond:
4093                    info.append(splitLine)
4094
4095                info.append(
4096                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4097                        "  √" if bond[1]["paid"] else "  —",
4098                        bond[1]["couponDate"].split("T")[0],
4099                        bond[1]["figi"],
4100                        bond[1]["ticker"],
4101                        bond[1]["couponNumber"],
4102                        "{} {}".format(
4103                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4104                            bond[1]["payCurrency"],
4105                        ),
4106                        bond[1]["couponType"],
4107                        bond[1]["couponPeriod"],
4108                        bond[1]["fixDate"].split("T")[0],
4109                    )
4110                )
4111
4112                if i < len(calendar.values) - 1:
4113                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4114                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4115                    newMonth = False if curDate.month == nextDate.month else True
4116
4117                else:
4118                    newMonth = False
4119
4120            infoText += "".join(info)
4121
4122            if show:
4123                uLogger.info("{}".format(infoText))
4124
4125            if self.calendarFile is not None:
4126                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4127                    fH.write(infoText)
4128
4129                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4130
4131        else:
4132            infoText += "No data\n"
4133
4134        return infoText
4135
4136    def OverviewAccounts(self, show: bool = False) -> dict:
4137        """
4138        Method for parsing and show simple table with all available user accounts.
4139
4140        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4141
4142        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4143        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4144                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4145                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4146                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4147                                                        "closed": "—", "access": "Full access" }, ...}}`
4148        """
4149        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4150
4151        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4152        accounts = {
4153            item["id"]: {
4154                "type": TKS_ACCOUNT_TYPES[item["type"]],
4155                "name": item["name"],
4156                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4157                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4158                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4159                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4160            } for item in rawAccounts["accounts"]
4161        }
4162
4163        # Raw and parsed data with some fields replaced in "stat" section:
4164        view = {
4165            "rawAccounts": rawAccounts,
4166            "stat": accounts,
4167        }
4168
4169        # --- Prepare simple text table with only accounts data in human-readable format:
4170        if show:
4171            info = [
4172                "# User accounts\n\n",
4173                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4174                "| Account ID   | Type                      | Status                    | Name                           |\n",
4175                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4176            ]
4177
4178            for account in view["stat"].keys():
4179                info.extend([
4180                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4181                        account,
4182                        view["stat"][account]["type"],
4183                        view["stat"][account]["status"],
4184                        view["stat"][account]["name"],
4185                    )
4186                ])
4187
4188            infoText = "".join(info)
4189
4190            uLogger.info(infoText)
4191
4192            if self.userAccountsFile:
4193                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4194                    fH.write(infoText)
4195
4196                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4197
4198        return view
4199
4200    def OverviewUserInfo(self, show: bool = False) -> dict:
4201        """
4202        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4203
4204        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4205
4206        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4207        :return: dict with raw parsed data from server and some calculated statistics about it.
4208        """
4209        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4210        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4211        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4212        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4213        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4214        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4215
4216        # This is dict with parsed common user data:
4217        userInfo = {
4218            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4219            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4220            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4221            "tariff": rawUserInfo["tariff"],
4222        }
4223
4224        # This is an array of dict with parsed margin statuses for every account IDs:
4225        margins = {}
4226        for accountId in accounts.keys():
4227            if rawMargins[accountId]:
4228                margins[accountId] = {
4229                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4230                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4231                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4232                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4233                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4234                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4235                }
4236
4237            else:
4238                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4239
4240        unary = {}  # unary-connection limits
4241        for item in rawTariffLimits["unaryLimits"]:
4242            if item["limitPerMinute"] in unary.keys():
4243                unary[item["limitPerMinute"]].extend(item["methods"])
4244
4245            else:
4246                unary[item["limitPerMinute"]] = item["methods"]
4247
4248        stream = {}  # stream-connection limits
4249        for item in rawTariffLimits["streamLimits"]:
4250            if item["limit"] in stream.keys():
4251                stream[item["limit"]].extend(item["streams"])
4252
4253            else:
4254                stream[item["limit"]] = item["streams"]
4255
4256        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4257        limits = {
4258            "unary": unary,
4259            "stream": stream,
4260        }
4261
4262        # Raw and parsed data as an output result:
4263        view = {
4264            "rawUserInfo": rawUserInfo,
4265            "rawAccounts": rawAccounts,
4266            "rawMargins": rawMargins,
4267            "rawTariffLimits": rawTariffLimits,
4268            "stat": {
4269                "userInfo": userInfo,
4270                "accounts": accounts,
4271                "margins": margins,
4272                "limits": limits,
4273            },
4274        }
4275
4276        # --- Prepare text table with user information in human-readable format:
4277        if show:
4278            info = [
4279                "# Full user information\n\n",
4280                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4281                "## Common information\n\n",
4282                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4283                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4284                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4285                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4286                "\n## User accounts\n\n",
4287            ]
4288
4289            for account in view["stat"]["accounts"].keys():
4290                info.extend([
4291                    "### ID: [{}]\n\n".format(account),
4292                    "| Parameters           | Values                                                       |\n",
4293                    "|----------------------|--------------------------------------------------------------|\n",
4294                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4295                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4296                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4297                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4298                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4299                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4300                ])
4301
4302                if margins[account]:
4303                    info.extend([
4304                        "| Margin status:       | Enabled                                                      |\n",
4305                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4306                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4307                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4308                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4309                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4310                    ])
4311
4312                else:
4313                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4314
4315            info.extend([
4316                "\n## Current user tariff limits\n",
4317                "\nSee also:\n",
4318                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4319                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4320                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4321                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4322                "\n### Unary limits\n",
4323            ])
4324
4325            if unary:
4326                for key, values in sorted(unary.items()):
4327                    info.append("\n* Max requests per minute: {}\n".format(key))
4328
4329                    for value in values:
4330                        info.append("  - {}\n".format(value))
4331
4332            else:
4333                info.append("\nNot available\n")
4334
4335            info.append("\n### Stream limits\n")
4336
4337            if stream:
4338                for key, values in sorted(stream.items()):
4339                    info.append("\n* Max stream connections: {}\n".format(key))
4340
4341                    for value in values:
4342                        info.append("  - {}\n".format(value))
4343
4344            else:
4345                info.append("\nNot available\n")
4346
4347            infoText = "".join(info)
4348
4349            uLogger.info(infoText)
4350
4351            if self.userInfoFile:
4352                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4353                    fH.write(infoText)
4354
4355                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4356
4357        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
198    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
199        """
200        Main class init.
201
202        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
203        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
204                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
205        :param useCache: use default cache file with raw data to use instead of `iList`.
206                         True by default. Cache is auto-update if new day has come.
207                         If you don't want to use cache and always updates raw data then set `useCache=False`.
208        :param defaultCache: path to default cache file. `dump.json` by default.
209        """
210        if token is None or not token:
211            try:
212                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
213                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
214
215            except KeyError:
216                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
217                raise Exception("Token required")
218
219        else:
220            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
221            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
222
223        if accountId is None or not accountId:
224            try:
225                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
226                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
227
228            except KeyError:
229                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
230
231        else:
232            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
233            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
234
235        self.version = __version__  # duplicate here used TKSBrokerAPI main version
236        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
237
238        Latest version: https://pypi.org/project/tksbrokerapi/
239        """
240
241        self.aliases = TKS_TICKER_ALIASES
242        """Some aliases instead official tickers.
243
244        See also: `TKSEnums.TKS_TICKER_ALIASES`
245        """
246
247        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
248
249        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
250
251        self.ticker = ""
252        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
253
254        See also: `SearchByTicker()`, `SearchInstruments()`.
255        """
256
257        self.figi = ""
258        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
259
260        See also: `SearchByFIGI()`, `SearchInstruments()`.
261        """
262
263        self.depth = 1
264        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
265
266        See also: `GetCurrentPrices()`.
267        """
268
269        self.server = r"https://invest-public-api.tinkoff.ru/rest"
270        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
271
272        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
273        """
274
275        uLogger.debug("Broker API server: {}".format(self.server))
276
277        self.timeout = 15
278        """Server operations timeout in seconds. Default: `15`.
279
280        See also: `SendAPIRequest()`.
281        """
282
283        self.headers = {
284            "Content-Type": "application/json",
285            "accept": "application/json",
286            "Authorization": "Bearer {}".format(self.token),
287            "x-app-name": "Tim55667757.TKSBrokerAPI",
288        }
289        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
290
291        See also: `SendAPIRequest()`.
292        """
293
294        self.body = None
295        """Request body which send to broker server. Default: `None`.
296
297        See also: `SendAPIRequest()`.
298        """
299
300        self.historyFile = None
301        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
302
303        See also: `History()`.
304        """
305
306        self.htmlHistoryFile = "index.html"
307        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
308
309        See also: `ShowHistoryChart()`.
310        """
311
312        self.instrumentsFile = "instruments.md"
313        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
314
315        See also: `ShowInstrumentsInfo()`.
316        """
317
318        self.searchResultsFile = "search-results.md"
319        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
320
321        See also: `SearchInstruments()`.
322        """
323
324        self.pricesFile = "prices.md"
325        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
326
327        See also: `GetListOfPrices()`.
328        """
329
330        self.infoFile = "info.md"
331        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
332
333        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
334        """
335
336        self.bondsXLSXFile = "ext-bonds.xlsx"
337        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
338        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
339
340        See also: `ExtendBondsData()`.
341        """
342
343        self.calendarFile = "calendar.md"
344        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
345        
346        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
347
348        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
349        """
350
351        self.overviewFile = "overview.md"
352        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
353
354        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
355        """
356
357        self.overviewDigestFile = "overview-digest.md"
358        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
359
360        See also: `Overview()` with parameter `details="digest"`.
361        """
362
363        self.overviewPositionsFile = "overview-positions.md"
364        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
365
366        See also: `Overview()` with parameter `details="positions"`.
367        """
368
369        self.overviewOrdersFile = "overview-orders.md"
370        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
371
372        See also: `Overview()` with parameter `details="orders"`.
373        """
374
375        self.overviewAnalyticsFile = "overview-analytics.md"
376        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
377
378        See also: `Overview()` with parameter `details="analytics"`.
379        """
380
381        self.reportFile = "deals.md"
382        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
383
384        See also: `Deals()`.
385        """
386
387        self.withdrawalLimitsFile = "limits.md"
388        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
389
390        See also: `OverviewLimits()` and `RequestLimits()`.
391        """
392
393        self.userInfoFile = "user-info.md"
394        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
395
396        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
397        """
398
399        self.userAccountsFile = "accounts.md"
400        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
401
402        See also: `OverviewAccounts()`, `RequestAccounts()`.
403        """
404
405        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
406        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
407
408        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
409
410        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
411        """
412
413        self.iList = None  # init iList for raw instruments data
414        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
415        
416        See also: `Listing()`, `DumpInstruments()`.
417        """
418
419        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
420        if useCache:
421            if os.path.exists(self.iListDumpFile):
422                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
423                curTime = datetime.now(tzutc())
424
425                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
426                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
427
428                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
429
430                else:
431                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
432
433                    uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile)))
434                    uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
435
436            else:
437                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
438                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
439
440        else:
441            self.iList = self.Listing()  # request new raw instruments data from broker server
442            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
443
444        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
445        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
446
447        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
448        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

ticker

String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi

String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.

See also: SearchByFIGI(), SearchInstruments().

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
472    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
473        """
474        Send GET or POST request to broker server and receive JSON object.
475
476        self.header: must be defining with dictionary of headers.
477        self.body: if define then used as request body. None by default.
478        self.timeout: global request timeout, 15 seconds by default.
479        :param url: url with REST request.
480        :param reqType: send "GET" or "POST" request. "GET" by default.
481        :param retry: how many times retry after first request if an 5xx server errors occurred.
482        :param pause: sleep time in seconds between retries.
483        :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc.
484        :return: response JSON (dictionary) from broker.
485        """
486        if reqType not in ("GET", "POST"):
487            uLogger.error("You can define request type: 'GET' or 'POST'!")
488            raise Exception("Incorrect value")
489
490        if debug:
491            uLogger.debug("Request parameters:")
492            uLogger.debug("    - REST API URL: {}".format(url))
493            uLogger.debug("    - request type: {}".format(reqType))
494            uLogger.debug("    - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***")))
495            uLogger.debug("    - body: {}".format(self.body))
496
497        # fast hack to avoid all operations with some tickers/FIGI
498        responseJSON = {}
499        oK = True
500        for item in self.exclude:
501            if item in url:
502                if debug:
503                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
504
505                oK = False
506                break
507
508        if oK:
509            counter = 0
510            response = None
511            errMsg = ""
512
513            while not response and counter <= retry:
514                if reqType == "GET":
515                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
516
517                if reqType == "POST":
518                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
519
520                if debug:
521                    uLogger.debug("Response:")
522                    uLogger.debug("    - status code: {}".format(response.status_code))
523                    uLogger.debug("    - reason: {}".format(response.reason))
524                    uLogger.debug("    - body length: {}".format(len(response.text)))
525                    uLogger.debug("    - headers: {}".format(response.headers))
526
527                # Server returns some headers:
528                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
529                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
530                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
531                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
532                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
533                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
534                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
535                    sleep(rateLimitWait)
536
537                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
538                if 400 <= response.status_code < 500:
539                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
540                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
541                    counter = retry + 1
542
543                if 500 <= response.status_code < 600:
544                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
545                    uLogger.debug("    - not oK, {}".format(errMsg))
546                    counter += 1
547
548                    if counter <= retry:
549                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
550                        sleep(pause)
551
552            responseJSON = self._ParseJSON(response.text)
553
554            if errMsg:
555                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
556                uLogger.error("    - not oK, {}".format(errMsg))
557
558        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
  • debug: if True then print more debug information, e.g. request and response parameters, headers etc.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
591    def Listing(self) -> dict:
592        """
593        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
594
595        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
596        """
597        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
598        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
599
600        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
601        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
602        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
603
604        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
605        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
606        poolUpdater.close()
607
608        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
609        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
610        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
611
612        # calculate minimum price increment (step) for all instruments and set up instrument's type:
613        for iType in iList.keys():
614            for ticker in iList[iType]:
615                iList[iType][ticker]["type"] = iType
616
617                if "minPriceIncrement" in iList[iType][ticker].keys():
618                    iList[iType][ticker]["step"] = NanoToFloat(
619                        iList[iType][ticker]["minPriceIncrement"]["units"],
620                        iList[iType][ticker]["minPriceIncrement"]["nano"],
621                    )
622
623                else:
624                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
625
626        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
628    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
629        """
630        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
631
632        See also: `DumpInstruments()`, `Listing()`.
633
634        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
635                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
636        """
637        if self.iListDumpFile is None or not self.iListDumpFile:
638            uLogger.error("Output name of dump file must be defined!")
639            raise Exception("Filename required")
640
641        if not self.iList or forceUpdate:
642            self.iList = self.Listing()
643
644        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
645
646        # Save as XLSX with separated sheets for every type of instruments:
647        with pd.ExcelWriter(
648                path=xlsxDumpFile,
649                date_format=TKS_DATE_FORMAT,
650                datetime_format=TKS_DATE_TIME_FORMAT,
651                mode="w",
652        ) as writer:
653            for iType in TKS_INSTRUMENTS:
654                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
655                df = df[sorted(df)]  # sorted by column names
656                df = df.applymap(
657                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
658                    na_action="ignore",
659                )  # converting numbers from nano-type to float in every cell
660                df.to_excel(
661                    writer,
662                    sheet_name=iType,
663                    encoding="UTF-8",
664                    freeze_panes=(1, 1),
665                )  # saving as XLSX-file with freeze first row and column as headers
666
667        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
669    def DumpInstruments(self, forceUpdate: bool = True) -> str:
670        """
671        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
672        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
673
674        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
675
676        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
677                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
678        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
679        """
680        if self.iListDumpFile is None or not self.iListDumpFile:
681            uLogger.error("Output name of dump file must be defined!")
682            raise Exception("Filename required")
683
684        if not self.iList or forceUpdate:
685            self.iList = self.Listing()
686
687        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
688        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
689            fH.write(jsonDump)
690
691        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
692
693        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
695    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
696        """
697        Show information about one instrument defined by json data and prints it in Markdown format.
698
699        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
700
701        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
702        :param show: if `True` then also printing information about instrument and its current price.
703        :return: multilines text in Markdown format with information about one instrument.
704        """
705        splitLine = "|                                                             |                                                        |\n"
706        infoText = ""
707
708        if iJSON is not None and iJSON and isinstance(iJSON, dict):
709            info = [
710                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
711                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
712                "| Parameters                                                  | Values                                                 |\n",
713                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
714                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
715                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
716            ]
717
718            if "sector" in iJSON.keys() and iJSON["sector"]:
719                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
720
721            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
722                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
723                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
724            )))
725
726            info.extend([
727                splitLine,
728                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
729                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
730            ])
731
732            if "isin" in iJSON.keys() and iJSON["isin"]:
733                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
734
735            if "classCode" in iJSON.keys():
736                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
737
738            info.extend([
739                splitLine,
740                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
741                splitLine,
742                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
743                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
744                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
745            ])
746
747            if iJSON["figi"]:
748                self.figi = iJSON["figi"]
749                iJSON = iJSON | self.RequestTradingStatus()
750
751                info.extend([
752                    splitLine,
753                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
754                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
755                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
756                ])
757
758            info.append(splitLine)
759
760            if "type" in iJSON.keys() and iJSON["type"]:
761                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
762
763            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
764                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
765
766            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
767                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
768
769            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
770                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
771
772            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
773                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
774
775            if "focusType" in iJSON.keys() and iJSON["focusType"]:
776                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
777
778            if "assetType" in iJSON.keys() and iJSON["assetType"]:
779                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
780
781            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
782                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
783
784            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
785                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
786
787            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
788                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
789
790            if "currency" in iJSON.keys():
791                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
792
793            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
794                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
795
796            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
797                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
798
799            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
800                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
801
802            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
803                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
804
805            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
806                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
807
808            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
809                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
810
811            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
812                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
813
814            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
815                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
816
817            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
818                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
819
820            iExt = None
821            if iJSON["type"] == "Bonds":
822                info.extend([
823                    splitLine,
824                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
825                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
826                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
827                        iJSON["nominal"]["currency"],
828                    )),
829                ])
830
831                if "floatingCouponFlag" in iJSON.keys():
832                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
833
834                if "amortizationFlag" in iJSON.keys():
835                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
836
837                info.append(splitLine)
838
839                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
840                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
841
842                iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
843
844                info.extend([
845                    "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
846                    "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
847                    "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
848                ])
849
850                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
851                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
852                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
853                        iJSON["aciValue"]["currency"]
854                    )))
855
856            if "currentPrice" in iJSON.keys():
857                info.append(splitLine)
858
859                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
860                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
861
862                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
863                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
864                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
865                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
866                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
867
868                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
869                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
870
871                info.extend([
872                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
873                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
874                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
875                    )),
876                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
877                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
878                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
879                    )),
880                    "| Changes between last deal price and last close              | {:<54} |\n".format(
881                        "{:.2f}%{}".format(
882                            iJSON["currentPrice"]["changes"],
883                            " ({}{:.2f} {})".format(
884                                "+" if bondChangesDelta > 0 else "",
885                                bondChangesDelta,
886                                aciCurrency
887                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
888                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
889                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
890                                currency
891                            ),
892                        )
893                    ),
894                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
895                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
896                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
897                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
898                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
899                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
900                    )),
901                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
902                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
903                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
904                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
905                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
906                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
907                    )),
908                ])
909
910            if "lot" in iJSON.keys():
911                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
912
913            if "step" in iJSON.keys() and iJSON["step"] != 0:
914                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
915
916            # Add bond payment calendar:
917            if iJSON["type"] == "Bonds":
918                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
919                info.extend(["\n", strCalendar])
920
921            infoText += "".join(info)
922
923            if show:
924                uLogger.info("{}".format(infoText))
925
926            else:
927                uLogger.debug("{}".format(infoText))
928
929            if self.infoFile is not None:
930                with open(self.infoFile, "w", encoding="UTF-8") as fH:
931                    fH.write(infoText)
932
933                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
934
935        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self.ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker( self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 937    def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 938        """
 939        Search and return raw broker's information about instrument by its ticker.
 940        `ticker` must be defined! If debug=True then print all debug messages.
 941
 942        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 943        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 944        :param debug: if `True` then print all debug console messages.
 945        :return: JSON formatted data with information about instrument.
 946        """
 947        tickerJSON = {}
 948        if debug:
 949            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 950
 951        if not self.ticker:
 952            uLogger.warning("self.ticker variable is not be empty!")
 953
 954        else:
 955            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 956                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 957                raise Exception("Instrument not allowed")
 958
 959            if not self.iList:
 960                self.iList = self.Listing()
 961
 962            if self.ticker in self.iList["Shares"].keys():
 963                tickerJSON = self.iList["Shares"][self.ticker]
 964                if debug:
 965                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 966
 967            elif self.ticker in self.iList["Currencies"].keys():
 968                tickerJSON = self.iList["Currencies"][self.ticker]
 969                if debug:
 970                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 971
 972            elif self.ticker in self.iList["Bonds"].keys():
 973                tickerJSON = self.iList["Bonds"][self.ticker]
 974                if debug:
 975                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 976
 977            elif self.ticker in self.iList["Etfs"].keys():
 978                tickerJSON = self.iList["Etfs"][self.ticker]
 979                if debug:
 980                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 981
 982            elif self.ticker in self.iList["Futures"].keys():
 983                tickerJSON = self.iList["Futures"][self.ticker]
 984                if debug:
 985                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 986
 987        if tickerJSON:
 988            self.figi = tickerJSON["figi"]
 989
 990            if requestPrice:
 991                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 992
 993                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 994                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 995
 996                else:
 997                    tickerJSON["currentPrice"]["changes"] = 0
 998
 999            if show:
1000                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
1001
1002        else:
1003            if show:
1004                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1005
1006        return tickerJSON

Search and return raw broker's information about instrument by its ticker. ticker must be defined! If debug=True then print all debug messages.

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
  • debug: if True then print all debug console messages.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI( self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1008    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1009        """
1010        Search and return raw broker's information about instrument by its FIGI.
1011        `figi` must be defined! If debug=True then print all debug messages.
1012
1013        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1014        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1015        :param debug: if `True` then print all debug console messages.
1016        :return: JSON formatted data with information about instrument.
1017        """
1018        figiJSON = {}
1019        if debug:
1020            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1021
1022        if not self.figi:
1023            uLogger.warning("self.figi variable is not be empty!")
1024
1025        else:
1026            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1027                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1028                raise Exception("Instrument not allowed")
1029
1030            if not self.iList:
1031                self.iList = self.Listing()
1032
1033            for item in self.iList["Shares"].keys():
1034                if self.figi == self.iList["Shares"][item]["figi"]:
1035                    figiJSON = self.iList["Shares"][item]
1036
1037                    if debug:
1038                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1039
1040                    break
1041
1042            if not figiJSON:
1043                for item in self.iList["Currencies"].keys():
1044                    if self.figi == self.iList["Currencies"][item]["figi"]:
1045                        figiJSON = self.iList["Currencies"][item]
1046
1047                        if debug:
1048                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1049
1050                        break
1051
1052            if not figiJSON:
1053                for item in self.iList["Bonds"].keys():
1054                    if self.figi == self.iList["Bonds"][item]["figi"]:
1055                        figiJSON = self.iList["Bonds"][item]
1056
1057                        if debug:
1058                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1059
1060                        break
1061
1062            if not figiJSON:
1063                for item in self.iList["Etfs"].keys():
1064                    if self.figi == self.iList["Etfs"][item]["figi"]:
1065                        figiJSON = self.iList["Etfs"][item]
1066
1067                        if debug:
1068                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1069
1070                        break
1071
1072            if not figiJSON:
1073                for item in self.iList["Futures"].keys():
1074                    if self.figi == self.iList["Futures"][item]["figi"]:
1075                        figiJSON = self.iList["Futures"][item]
1076
1077                        if debug:
1078                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1079
1080                        break
1081
1082        if figiJSON:
1083            self.figi = figiJSON["figi"]
1084            self.ticker = figiJSON["ticker"]
1085
1086            if requestPrice:
1087                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1088
1089                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1090                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1091
1092                else:
1093                    figiJSON["currentPrice"]["changes"] = 0
1094
1095            if show:
1096                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1097
1098        else:
1099            if show:
1100                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1101
1102        return figiJSON

Search and return raw broker's information about instrument by its FIGI. figi must be defined! If debug=True then print all debug messages.

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
  • debug: if True then print all debug console messages.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1104    def GetCurrentPrices(self, show: bool = True) -> dict:
1105        """
1106        Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
1107        `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1108
1109        See also:
1110
1111        :param show: if `True` then print DOM to log and console.
1112        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1113        """
1114        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1115
1116        if self.depth < 1:
1117            uLogger.error("Depth of Market (DOM) must be >=1!")
1118            raise Exception("Incorrect value")
1119
1120        if not (self.ticker or self.figi):
1121            uLogger.error("self.ticker or self.figi variables must be defined!")
1122            raise Exception("Ticker or FIGI required")
1123
1124        if self.ticker and not self.figi:
1125            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1126            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1127
1128        if not self.ticker and self.figi:
1129            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1130            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1131
1132        if not self.figi:
1133            uLogger.error("FIGI is not defined!")
1134            raise Exception("Ticker or FIGI required")
1135
1136        else:
1137            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1138
1139            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1140            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1141            self.body = str({"figi": self.figi, "depth": self.depth})
1142            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")
1143
1144            if pricesResponse:
1145                # list of dicts with sellers orders:
1146                prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1147
1148                # list of dicts with buyers orders:
1149                prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1150
1151                # max price of instrument at this time:
1152                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1153
1154                # min price of instrument at this time:
1155                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1156
1157                # last price of deal with instrument:
1158                prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0
1159
1160                # last close price of instrument:
1161                prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0
1162
1163            else:
1164                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1165                uLogger.debug("Server response: {}".format(pricesResponse))
1166
1167            if show:
1168                if prices["buy"] or prices["sell"]:
1169                    info = [
1170                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1171                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1172                            self.ticker,
1173                            self.figi,
1174                            self.depth,
1175                        ),
1176                        uLog.sepShort, "\n",
1177                        " Orders of Buyers   | Orders of Sellers\n",
1178                        uLog.sepShort, "\n",
1179                        " Sell prices (vol.) | Buy prices (vol.)\n",
1180                        uLog.sepShort, "\n",
1181                    ]
1182
1183                    if not prices["buy"]:
1184                        info.append("                    | No orders!\n")
1185                        sumBuy = 0
1186
1187                    else:
1188                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1189                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1190                        for item in maxMinSorted:
1191                            info.append("                    | {} ({})\n".format(item["price"], item["quantity"]))
1192
1193                    if not prices["sell"]:
1194                        info.append("No orders!          |\n")
1195                        sumSell = 0
1196
1197                    else:
1198                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1199                        for item in prices["sell"]:
1200                            info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1201
1202                    info.extend([
1203                        uLog.sepShort, "\n",
1204                        "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1205                        uLog.sepShort, "\n",
1206                    ])
1207
1208                    infoText = "".join(info)
1209
1210                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1211
1212                else:
1213                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1214
1215        return prices

Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

See also:

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1217    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1218        """
1219        This method get and show information about all available broker instruments for current user account.
1220        If `instrumentsFile` string is not empty then also save information to this file.
1221
1222        :param show: if `True` then print results to console, if `False` - print only to file.
1223        :return: multi-lines string with all available broker instruments
1224        """
1225        if not self.iList:
1226            self.iList = self.Listing()
1227
1228        info = [
1229            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1230            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1231        ]
1232
1233        # add instruments count by type:
1234        for iType in self.iList.keys():
1235            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1236
1237        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1238        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1239
1240        # generating info tables with all instruments by type:
1241        for iType in self.iList.keys():
1242            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1243
1244            for instrument in self.iList[iType].keys():
1245                iName = self.iList[iType][instrument]["name"]  # instrument's name
1246                if len(iName) > 57:
1247                    iName = "{}...".format(iName[:54])  # right trim for a long string
1248
1249                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1250                    self.iList[iType][instrument]["ticker"],
1251                    iName,
1252                    self.iList[iType][instrument]["figi"],
1253                    self.iList[iType][instrument]["currency"],
1254                    self.iList[iType][instrument]["lot"],
1255                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1256                ))
1257
1258        infoText = "".join(info)
1259
1260        if show:
1261            uLogger.info(infoText)
1262
1263        if self.instrumentsFile:
1264            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1265                fH.write(infoText)
1266
1267            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1268
1269        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False - print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1271    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1272        """
1273        This method search and show information about instruments by part of its ticker, FIGI or name.
1274        If `searchResultsFile` string is not empty then also save information to this file.
1275
1276        :param pattern: string with part of ticker, FIGI or instrument's name.
1277        :param show: if `True` then print results to console, if `False` - return list of result only.
1278        :return: list of dictionaries with all found instruments.
1279        """
1280        if not self.iList:
1281            self.iList = self.Listing()
1282
1283        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1284        compiledPattern = re.compile(pattern, re.IGNORECASE)
1285
1286        for iType in self.iList:
1287            for instrument in self.iList[iType].values():
1288                searchResult = compiledPattern.search(" ".join(
1289                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1290                ))
1291
1292                if searchResult:
1293                    searchResults[iType][instrument["ticker"]] = instrument
1294
1295        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1296        info = [
1297            "# Search results\n\n",
1298            "* **Search pattern:** [{}]\n".format(pattern),
1299            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1300            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1301        ]
1302        infoShort = info[:]
1303
1304        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1305        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1306        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1307
1308        if resultsLen == 0:
1309            info.append("\nNo results\n")
1310            infoShort.append("\nNo results\n")
1311            uLogger.warning("No results. Try changing your search pattern.")
1312
1313        else:
1314            for iType in searchResults:
1315                iTypeValuesCount = len(searchResults[iType].values())
1316                if iTypeValuesCount > 0:
1317                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1318                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1319
1320                    for instrument in searchResults[iType].values():
1321                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1322                            instrument["type"],
1323                            instrument["ticker"],
1324                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1325                            instrument["figi"],
1326                        ))
1327
1328                    if iTypeValuesCount <= 5:
1329                        infoShort.extend(info[-iTypeValuesCount:])
1330
1331                    else:
1332                        infoShort.extend(info[-5:])
1333                        infoShort.append(skippedLine)
1334
1335        infoText = "".join(info)
1336        infoTextShort = "".join(infoShort)
1337
1338        if show:
1339            uLogger.info(infoTextShort)
1340            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1341
1342        if self.searchResultsFile:
1343            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1344                fH.write(infoText)
1345
1346            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1347
1348        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False - return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1350    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1351        """
1352        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1353
1354        :param instruments: list of strings with tickers or FIGIs.
1355        :return: list with unique instrument FIGIs only.
1356        """
1357        requestedInstruments = []
1358        for iName in instruments:
1359            if iName not in self.aliases.keys():
1360                if iName not in requestedInstruments:
1361                    requestedInstruments.append(iName)
1362
1363            else:
1364                if iName not in requestedInstruments:
1365                    if self.aliases[iName] not in requestedInstruments:
1366                        requestedInstruments.append(self.aliases[iName])
1367
1368        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1369
1370        onlyUniqueFIGIs = []
1371        for iName in requestedInstruments:
1372            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1373                continue
1374
1375            self.ticker = iName
1376            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1377
1378            if not iData:
1379                self.ticker = ""
1380                self.figi = iName
1381
1382                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1383
1384                if not iData:
1385                    self.figi = ""
1386                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1387
1388            if iData and iData["figi"] not in onlyUniqueFIGIs:
1389                onlyUniqueFIGIs.append(iData["figi"])
1390
1391        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1392
1393        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1395    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1396        """
1397        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1398        See limits: https://tinkoff.github.io/investAPI/limits/
1399        If `pricesFile` string is not empty then also save information to this file.
1400
1401        :param instruments: list of strings with tickers or FIGIs.
1402        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1403        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1404                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1405        """
1406        if instruments is None or not instruments:
1407            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1408            raise Exception("Ticker or FIGI required")
1409
1410        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1411
1412        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1413
1414        iList = []  # trying to get info and current prices about all unique instruments:
1415        for self.figi in onlyUniqueFIGIs:
1416            iData = self.SearchByFIGI(requestPrice=True)
1417            iList.append(iData)
1418
1419        self.ShowListOfPrices(iList, show)
1420
1421        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! See limits: https://tinkoff.github.io/investAPI/limits/ If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False - prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1423    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1424        """
1425        Show table contains current prices of given instruments.
1426
1427        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1428                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1429        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1430        :return: multilines text in Markdown format as a table contains current prices.
1431        """
1432        infoText = ""
1433
1434        if show or self.pricesFile:
1435            info = [
1436                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1437                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1438                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1439            ]
1440
1441            for item in iList:
1442                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1443                    item["ticker"],
1444                    item["figi"],
1445                    item["type"],
1446                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1447                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1448                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1449                    "{} / {}".format(
1450                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1451                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1452                    ),
1453                    "{} / {}".format(
1454                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1455                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1456                    ),
1457                    item["currency"],
1458                ))
1459
1460            infoText = "".join(info)
1461
1462            if show:
1463                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1464
1465            if self.pricesFile:
1466                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1467                    fH.write(infoText)
1468
1469                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1470
1471        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False - prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1473    def RequestTradingStatus(self) -> dict:
1474        """
1475        Requesting trading status for the instrument defined by `figi` variable.
1476        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1477        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1478
1479        :return: dictionary with trading status attributes. Response example:
1480                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1481                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1482        """
1483        if self.figi is None or not self.figi:
1484            uLogger.error("Variable `figi` must be defined for using this method!")
1485            raise Exception("FIGI required")
1486
1487        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1488
1489        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1490        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1491        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1492
1493        uLogger.debug("Records about current trading status successfully received")
1494
1495        return tradingStatus

Requesting trading status for the instrument defined by figi variable. REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1497    def RequestPortfolio(self) -> dict:
1498        """
1499        Requesting actual user's portfolio for current `accountId`.
1500        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1501        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1502
1503        :return: dictionary with user's portfolio.
1504        """
1505        if self.accountId is None or not self.accountId:
1506            uLogger.error("Variable `accountId` must be defined for using this method!")
1507            raise Exception("Account ID required")
1508
1509        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1510
1511        self.body = str({"accountId": self.accountId})
1512        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1513        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1514
1515        uLogger.debug("Records about user's portfolio successfully received")
1516
1517        return rawPortfolio

Requesting actual user's portfolio for current accountId. REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1519    def RequestPositions(self) -> dict:
1520        """
1521        Requesting open positions by currencies and instruments for current `accountId`.
1522        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1523        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1524
1525        :return: dictionary with open positions by instruments.
1526        """
1527        if self.accountId is None or not self.accountId:
1528            uLogger.error("Variable `accountId` must be defined for using this method!")
1529            raise Exception("Account ID required")
1530
1531        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1532
1533        self.body = str({"accountId": self.accountId})
1534        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1535        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1536
1537        uLogger.debug("Records about current open positions successfully received")
1538
1539        return rawPositions

Requesting open positions by currencies and instruments for current accountId. REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1541    def RequestPendingOrders(self) -> list:
1542        """
1543        Requesting current actual pending orders for current `accountId`.
1544        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1545        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1546
1547        :return: list of dictionaries with pending orders.
1548        """
1549        if self.accountId is None or not self.accountId:
1550            uLogger.error("Variable `accountId` must be defined for using this method!")
1551            raise Exception("Account ID required")
1552
1553        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1554
1555        self.body = str({"accountId": self.accountId})
1556        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1557        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1558
1559        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1560
1561        return rawOrders

Requesting current actual pending orders for current accountId. REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending orders.

def RequestStopOrders(self) -> list:
1563    def RequestStopOrders(self) -> list:
1564        """
1565        Requesting current actual stop orders for current `accountId`.
1566        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1567        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1568
1569        :return: list of dictionaries with stop orders.
1570        """
1571        if self.accountId is None or not self.accountId:
1572            uLogger.error("Variable `accountId` must be defined for using this method!")
1573            raise Exception("Account ID required")
1574
1575        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1576
1577        self.body = str({"accountId": self.accountId})
1578        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1579        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1580
1581        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1582
1583        return rawStopOrders

Requesting current actual stop orders for current accountId. REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1585    def Overview(self, show: bool = False, details: str = "full") -> dict:
1586        """
1587        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1588        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1589        are defined then also save information to file.
1590
1591        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1592        many requests about the state of the portfolio, and then, based on the received data, a large number
1593        of calculation and statistics are collected.
1594
1595        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1596        :param details: how detailed should the information be? You should specify one of strings:
1597                        `full` - shows full available information about portfolio status (by default),
1598                        `positions` - shows only open positions,
1599                        `digest` - show a short digest of the portfolio status,
1600                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1601                        `orders` - shows only sections of open limits and stop orders.
1602        :return: dictionary with client's raw portfolio and some statistics.
1603        """
1604        if self.accountId is None or not self.accountId:
1605            uLogger.error("Variable `accountId` must be defined for using this method!")
1606            raise Exception("Account ID required")
1607
1608        view = {
1609            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1610                "headers": {},  # list of dictionaries, response headers without "positions" section
1611                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1612                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1613                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1614                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1615                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1616                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1617                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1618                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1619                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1620            },
1621            "stat": {  # --- some statistics calculated using "raw" sections:
1622                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1623                "availableRUB": 0.,  # available rubles (without other currencies)
1624                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1625                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1626                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1627                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1628                "sharesCostRUB": 0.,  # costs of all shares in RUB
1629                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1630                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1631                "futuresCostRUB": 0.,  # costs of all futures in RUB
1632                "Currencies": [],  # list of dictionaries of all currencies statistics
1633                "Shares": [],  # list of dictionaries of all shares statistics
1634                "Bonds": [],  # list of dictionaries of all bonds statistics
1635                "Etfs": [],  # list of dictionaries of all etfs statistics
1636                "Futures": [],  # list of dictionaries of all futures statistics
1637                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1638                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1639                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1640                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1641                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1642            },
1643            "analytics": {  # --- some analytics of portfolio:
1644                "distrByAssets": {},  # portfolio distribution by assets
1645                "distrByCompanies": {},  # portfolio distribution by companies
1646                "distrBySectors": {},  # portfolio distribution by sectors
1647                "distrByCurrencies": {},  # portfolio distribution by currencies
1648                "distrByCountries": {},  # portfolio distribution by countries
1649            }
1650        }
1651
1652        details = details.lower()
1653        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1654        if details not in availableDetails:
1655            details = "full"
1656            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1657
1658        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1659
1660        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1661        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1662        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1663        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1664
1665        # save response headers without "positions" section:
1666        for key in portfolioResponse.keys():
1667            if key != "positions":
1668                view["raw"]["headers"][key] = portfolioResponse[key]
1669
1670            else:
1671                continue
1672
1673        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1674        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1675        for item in portfolioResponse["positions"]:
1676            if item["instrumentType"] == "currency":
1677                self.figi = item["figi"]
1678                curr = self.SearchByFIGI(requestPrice=False)
1679
1680                # current price of currency in RUB:
1681                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1682                    "name": curr["name"],
1683                    "currentPrice": NanoToFloat(
1684                        item["currentPrice"]["units"],
1685                        item["currentPrice"]["nano"]
1686                    ),
1687                }
1688
1689                view["raw"]["Currencies"].append(item)
1690
1691            elif item["instrumentType"] == "share":
1692                view["raw"]["Shares"].append(item)
1693
1694            elif item["instrumentType"] == "bond":
1695                view["raw"]["Bonds"].append(item)
1696
1697            elif item["instrumentType"] == "etf":
1698                view["raw"]["Etfs"].append(item)
1699
1700            elif item["instrumentType"] == "futures":
1701                view["raw"]["Futures"].append(item)
1702
1703            else:
1704                continue
1705
1706        # how many volume of currencies (by ISO currency name) are blocked:
1707        for item in view["raw"]["positions"]["blocked"]:
1708            blocked = NanoToFloat(item["units"], item["nano"])
1709            if blocked > 0:
1710                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1711
1712        # how many volume of instruments (by FIGI) are blocked:
1713        for item in view["raw"]["positions"]["securities"]:
1714            blocked = int(item["blocked"])
1715            if blocked > 0:
1716                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1717
1718        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1719
1720        if "rub" in allBlocked.keys():
1721            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1722
1723        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1724        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1725        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1726        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1727        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1728        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1729        view["stat"]["portfolioCostRUB"] = sum([
1730            view["stat"]["allCurrenciesCostRUB"],
1731            view["stat"]["sharesCostRUB"],
1732            view["stat"]["bondsCostRUB"],
1733            view["stat"]["etfsCostRUB"],
1734            view["stat"]["futuresCostRUB"],
1735        ])
1736
1737        # --- calculating some portfolio statistics:
1738        byComp = {}  # distribution by companies
1739        bySect = {}  # distribution by sectors
1740        byCurr = {}  # distribution by currencies (include RUB)
1741        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1742        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1743
1744        for item in portfolioResponse["positions"]:
1745            self.figi = item["figi"]
1746            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1747
1748            if instrument:
1749                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1750                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1751
1752                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1753                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1754
1755                else:
1756                    blocked = 0
1757
1758                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1759                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1760                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1761                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1762                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1763                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1764                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1765                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1766                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1767                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1768                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1769                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1770
1771                statData = {
1772                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1773                    "ticker": instrument["ticker"],  # ticker by FIGI
1774                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1775                    "volume": volume,  # available volume of instrument
1776                    "lots": lots,  # volume in lots of instrument
1777                    "direction": direction,  # direction of an instrument's position: short or long
1778                    "blocked": blocked,  # blocked volume of currency or instrument
1779                    "currentPrice": curPrice,  # current instrument's price in basic asset
1780                    "average": average,  # current average position price
1781                    "cost": cost,  # current cost of all volume of instrument in basic asset
1782                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1783                    "costRUB": costRUB,  # cost of instrument in ruble
1784                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1785                    "profit": profit,  # expected profit at current moment
1786                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1787                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1788                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1789                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1790                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1791                    "step": instrument["step"],  # minimum price increment
1792                }
1793
1794                # adding distribution by unique countries:
1795                if statData["country"] not in byCountry.keys():
1796                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1797
1798                else:
1799                    byCountry[statData["country"]]["cost"] += costRUB
1800                    byCountry[statData["country"]]["percent"] += percentCostRUB
1801
1802                if item["instrumentType"] != "currency":
1803                    # adding distribution by unique companies:
1804                    if statData["name"]:
1805                        if statData["name"] not in byComp.keys():
1806                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1807
1808                        else:
1809                            byComp[statData["name"]]["cost"] += costRUB
1810                            byComp[statData["name"]]["percent"] += percentCostRUB
1811
1812                    # adding distribution by unique sectors:
1813                    if statData["sector"] not in bySect.keys():
1814                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1815
1816                    else:
1817                        bySect[statData["sector"]]["cost"] += costRUB
1818                        bySect[statData["sector"]]["percent"] += percentCostRUB
1819
1820                # adding distribution by unique currencies:
1821                if currency not in byCurr.keys():
1822                    byCurr[currency] = {
1823                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1824                        "cost": costRUB,
1825                        "percent": percentCostRUB
1826                    }
1827
1828                else:
1829                    byCurr[currency]["cost"] += costRUB
1830                    byCurr[currency]["percent"] += percentCostRUB
1831
1832                # saving statistics for every instrument:
1833                if item["instrumentType"] == "currency":
1834                    view["stat"]["Currencies"].append(statData)
1835
1836                    # update dict with free funds for trading (total - blocked) by currencies
1837                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1838                    view["stat"]["funds"][currency] = {
1839                        "total": volume,
1840                        "totalCostRUB": costRUB,  # total volume cost in rubles
1841                        "free": volume - blocked,
1842                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1843                    }
1844
1845                elif item["instrumentType"] == "share":
1846                    view["stat"]["Shares"].append(statData)
1847
1848                elif item["instrumentType"] == "bond":
1849                    view["stat"]["Bonds"].append(statData)
1850
1851                elif item["instrumentType"] == "etf":
1852                    view["stat"]["Etfs"].append(statData)
1853
1854                elif item["instrumentType"] == "Futures":
1855                    view["stat"]["Futures"].append(statData)
1856
1857                else:
1858                    continue
1859
1860        # total changes in Russian Ruble:
1861        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1862        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1863        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1864        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1865        view["stat"]["funds"]["rub"] = {
1866            "total": view["stat"]["availableRUB"],
1867            "totalCostRUB": view["stat"]["availableRUB"],
1868            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1869            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1870        }
1871
1872        # --- pending orders sector data:
1873        uniquePendingOrders = []
1874        uniquePendingOrdersFIGIs = []
1875        for item in view["raw"]["orders"]:
1876            if item["figi"] not in uniquePendingOrdersFIGIs:
1877                uniquePendingOrdersFIGIs.append(item["figi"])
1878                uniquePendingOrders.append(item)
1879
1880        for item in uniquePendingOrders:
1881            self.figi = item["figi"]
1882            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1883
1884            if instrument:
1885                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1886                orderType = TKS_ORDER_TYPES[item["orderType"]]
1887                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1888                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1889
1890                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1891                if item["direction"] == "ORDER_DIRECTION_BUY":
1892                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1893
1894                else:
1895                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1896
1897                # requested price for order execution:
1898                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1899
1900                # necessary changes in percent to reach target from current price:
1901                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1902
1903                view["stat"]["orders"].append({
1904                    "orderID": item["orderId"],  # orderId number parameter of current order
1905                    "figi": item["figi"],  # FIGI identification
1906                    "ticker": instrument["ticker"],  # ticker name by FIGI
1907                    "lotsRequested": item["lotsRequested"],  # requested lots value
1908                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1909                    "currentPrice": lastPrice,  # current instrument's price for defined action
1910                    "targetPrice": target,  # requested price for order execution in base currency
1911                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1912                    "percentChanges": changes,  # changes in percent to target from current price
1913                    "currency": item["currency"],  # instrument's currency name
1914                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1915                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1916                    "status": orderState,  # order status from TKS_ORDER_STATES
1917                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1918                })
1919
1920        # --- stop orders sector data:
1921        uniqueStopOrders = []
1922        uniqueStopOrdersFIGIs = []
1923        for item in view["raw"]["stopOrders"]:
1924            if item["figi"] not in uniqueStopOrdersFIGIs:
1925                uniqueStopOrdersFIGIs.append(item["figi"])
1926                uniqueStopOrders.append(item)
1927
1928        for item in uniqueStopOrders:
1929            self.figi = item["figi"]
1930            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1931
1932            if instrument:
1933                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1934                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1935                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1936
1937                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1938                if "expirationTime" in item.keys():
1939                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1940                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1941
1942                else:
1943                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1944                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1945
1946                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1947                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1948                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1949
1950                else:
1951                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1952
1953                # requested price when stop-order executed:
1954                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1955
1956                # price for limit-order, set up when stop-order executed:
1957                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1958
1959                # necessary changes in percent to reach target from current price:
1960                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1961
1962                view["stat"]["stopOrders"].append({
1963                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1964                    "figi": item["figi"],  # FIGI identification
1965                    "ticker": instrument["ticker"],  # ticker name by FIGI
1966                    "lotsRequested": item["lotsRequested"],  # requested lots value
1967                    "currentPrice": lastPrice,  # current instrument's price for defined action
1968                    "targetPrice": target,  # requested price for stop-order execution in base currency
1969                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1970                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1971                    "percentChanges": changes,  # changes in percent to target from current price
1972                    "currency": item["currency"],  # instrument's currency name
1973                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1974                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1975                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1976                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1977                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1978                })
1979
1980        # --- calculating data for analytics section:
1981        # portfolio distribution by assets:
1982        view["analytics"]["distrByAssets"] = {
1983            "Ruble": {
1984                "uniques": 1,
1985                "cost": view["stat"]["availableRUB"],
1986                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1987            },
1988            "Currencies": {
1989                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1990                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1991                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1992            },
1993            "Shares": {
1994                "uniques": len(view["stat"]["Shares"]),
1995                "cost": view["stat"]["sharesCostRUB"],
1996                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1997            },
1998            "Bonds": {
1999                "uniques": len(view["stat"]["Bonds"]),
2000                "cost": view["stat"]["bondsCostRUB"],
2001                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2002            },
2003            "Etfs": {
2004                "uniques": len(view["stat"]["Etfs"]),
2005                "cost": view["stat"]["etfsCostRUB"],
2006                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2007            },
2008            "Futures": {
2009                "uniques": len(view["stat"]["Futures"]),
2010                "cost": view["stat"]["futuresCostRUB"],
2011                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2012            },
2013        }
2014
2015        # portfolio distribution by companies:
2016        view["analytics"]["distrByCompanies"]["All money cash"] = {
2017            "ticker": "",
2018            "cost": view["stat"]["allCurrenciesCostRUB"],
2019            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2020        }
2021        view["analytics"]["distrByCompanies"].update(byComp)
2022
2023        # portfolio distribution by sectors:
2024        view["analytics"]["distrBySectors"]["All money cash"] = {
2025            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2026            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2027        }
2028        view["analytics"]["distrBySectors"].update(bySect)
2029
2030        # portfolio distribution by currencies:
2031        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2032            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2033            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2034
2035        view["analytics"]["distrByCurrencies"].update(byCurr)
2036        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2037        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2038
2039        # portfolio distribution by countries:
2040        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2041            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2042            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2043
2044        view["analytics"]["distrByCountries"].update(byCountry)
2045        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2046        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2047
2048        # --- Prepare text statistics overview in human-readable:
2049        if show:
2050            # Whatever the value `details`, header not changes:
2051            info = [
2052                "# Client's portfolio\n\n",
2053                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2054                "* **Account ID:** [{}]\n".format(self.accountId),
2055            ]
2056
2057            if details in ["full", "positions", "digest"]:
2058                info.extend([
2059                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2060                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2061                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2062                        view["stat"]["totalChangesRUB"],
2063                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2064                        view["stat"]["totalChangesPercentRUB"],
2065                    ),
2066                ])
2067
2068            if details in ["full", "positions"]:
2069                info.extend([
2070                    "## Open positions\n\n",
2071                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2072                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2073                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2074                        "{:.2f} ({:.2f}) rub".format(
2075                            view["stat"]["availableRUB"],
2076                            view["stat"]["blockedRUB"],
2077                        )
2078                    )
2079                ])
2080
2081                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2082                    return [
2083                        "|                             |                                 |          |              |              |                     |                              |\n",
2084                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2085                            noTradeStr if noTradeStr else typeStr,
2086                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2087                        ),
2088                    ]
2089
2090                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2091                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2092                        "{} [{}]".format(data["ticker"], data["figi"]),
2093                        "{:.2f} ({:.2f}) {}".format(
2094                            data["volume"],
2095                            data["blocked"],
2096                            data["currency"],
2097                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2098                            data["volume"],
2099                            data["blocked"],
2100                        ),
2101                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2102                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2103                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2104                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2105                        "{}{:.2f} {} ({}{:.2f}%)".format(
2106                            "+" if data["profit"] > 0 else "",
2107                            data["profit"], data["baseCurrencyName"],
2108                            "+" if data["percentProfit"] > 0 else "",
2109                            data["percentProfit"],
2110                        ),
2111                    )
2112
2113                # --- Show currencies section:
2114                if view["stat"]["Currencies"]:
2115                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2116                    for item in view["stat"]["Currencies"]:
2117                        info.append(_InfoStr(item, showCurrencyName=True))
2118
2119                else:
2120                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2121
2122                # --- Show shares section:
2123                if view["stat"]["Shares"]:
2124                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2125
2126                    for item in view["stat"]["Shares"]:
2127                        info.append(_InfoStr(item))
2128
2129                else:
2130                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2131
2132                # --- Show bonds section:
2133                if view["stat"]["Bonds"]:
2134                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2135
2136                    for item in view["stat"]["Bonds"]:
2137                        info.append(_InfoStr(item))
2138
2139                else:
2140                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2141
2142                # --- Show etfs section:
2143                if view["stat"]["Etfs"]:
2144                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2145
2146                    for item in view["stat"]["Etfs"]:
2147                        info.append(_InfoStr(item))
2148
2149                else:
2150                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2151
2152                # --- Show futures section:
2153                if view["stat"]["Futures"]:
2154                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2155
2156                    for item in view["stat"]["Futures"]:
2157                        info.append(_InfoStr(item))
2158
2159                else:
2160                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2161
2162            if details in ["full", "orders"]:
2163                # --- Show pending orders section:
2164                if view["stat"]["orders"]:
2165                    info.extend([
2166                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2167                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2168                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2169                    ])
2170
2171                    for item in view["stat"]["orders"]:
2172                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2173                            "{} [{}]".format(item["ticker"], item["figi"]),
2174                            item["orderID"],
2175                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2176                            "{} {} ({}{:.2f}%)".format(
2177                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2178                                item["baseCurrencyName"],
2179                                "+" if item["percentChanges"] > 0 else "",
2180                                float(item["percentChanges"]),
2181                            ),
2182                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2183                            item["action"],
2184                            item["type"],
2185                            item["date"],
2186                        ))
2187
2188                else:
2189                    info.append("\n## Total pending limit-orders: 0\n")
2190
2191                # --- Show stop orders section:
2192                if view["stat"]["stopOrders"]:
2193                    info.extend([
2194                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2195                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2196                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2197                    ])
2198
2199                    for item in view["stat"]["stopOrders"]:
2200                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2201                            "{} [{}]".format(item["ticker"], item["figi"]),
2202                            item["orderID"],
2203                            item["lotsRequested"],
2204                            "{} {} ({}{:.2f}%)".format(
2205                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2206                                item["baseCurrencyName"],
2207                                "+" if item["percentChanges"] > 0 else "",
2208                                float(item["percentChanges"]),
2209                            ),
2210                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2211                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2212                            item["action"],
2213                            item["type"],
2214                            item["expType"],
2215                            item["createDate"],
2216                            item["expDate"],
2217                        ))
2218
2219                else:
2220                    info.append("\n## Total stop-orders: 0\n")
2221
2222            if details in ["full", "analytics"]:
2223                # -- Show analytics section:
2224                if view["stat"]["portfolioCostRUB"] > 0:
2225                    info.extend([
2226                        "\n# Analytics\n"
2227                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2228                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2229                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2230                            view["stat"]["totalChangesRUB"],
2231                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2232                            view["stat"]["totalChangesPercentRUB"],
2233                        ),
2234                        "\n## Portfolio distribution by assets\n"
2235                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2236                        "|------------|---------|---------|--------------------|\n",
2237                    ])
2238
2239                    for key in view["analytics"]["distrByAssets"].keys():
2240                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2241                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2242                                key,
2243                                view["analytics"]["distrByAssets"][key]["uniques"],
2244                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2245                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2246                            ))
2247
2248                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2249                    info.extend([
2250                        "\n## Portfolio distribution by companies\n"
2251                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2252                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2253                    ])
2254
2255                    for company in view["analytics"]["distrByCompanies"].keys():
2256                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2257                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2258                            info.append("| {} | {:<7} | {:<18} |\n".format(
2259                                "{}{}{}".format(
2260                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2261                                    company,
2262                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2263                                ),
2264                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2265                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2266                            ))
2267
2268                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2269                    info.extend([
2270                        "\n## Portfolio distribution by sectors\n"
2271                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2272                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2273                    ])
2274
2275                    for sector in view["analytics"]["distrBySectors"].keys():
2276                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2277                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2278                                sector,
2279                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2280                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2281                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2282                            ))
2283
2284                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2285                    info.extend([
2286                        "\n## Portfolio distribution by currencies\n"
2287                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2288                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2289                    ])
2290
2291                    for curr in view["analytics"]["distrByCurrencies"].keys():
2292                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2293                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2294                            info.append("| {} | {:<7} | {:<18} |\n".format(
2295                                "[{}] {}{}".format(
2296                                    curr,
2297                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2298                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2299                                ),
2300                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2301                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2302                            ))
2303
2304                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2305                    info.extend([
2306                        "\n## Portfolio distribution by countries\n"
2307                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2308                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2309                    ])
2310
2311                    for country in view["analytics"]["distrByCountries"].keys():
2312                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2313                            nameLen = len(country)
2314                            info.append("| {} | {:<7} | {:<18} |\n".format(
2315                                "{}{}".format(
2316                                    country,
2317                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2318                                ),
2319                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2320                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2321                            ))
2322
2323            infoText = "".join(info)
2324
2325            uLogger.info(infoText)
2326
2327            if details == "full" and self.overviewFile:
2328                filename = self.overviewFile
2329
2330            elif details == "digest" and self.overviewDigestFile:
2331                filename = self.overviewDigestFile
2332
2333            elif details == "positions" and self.overviewPositionsFile:
2334                filename = self.overviewPositionsFile
2335
2336            elif details == "orders" and self.overviewOrdersFile:
2337                filename = self.overviewOrdersFile
2338
2339            elif details == "analytics" and self.overviewAnalyticsFile:
2340                filename = self.overviewAnalyticsFile
2341
2342            else:
2343                filename = ""
2344
2345            if filename:
2346                with open(filename, "w", encoding="UTF-8") as fH:
2347                    fH.write(infoText)
2348
2349                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2350
2351        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be? You should specify one of strings: full - shows full available information about portfolio status (by default), positions - shows only open positions, digest - show a short digest of the portfolio status, analytics - shows only the analytics section and the distribution of the portfolio by various categories, orders - shows only sections of open limits and stop orders.
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2353    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2354        """
2355        Returns history operations between two given dates for current `accountId`.
2356        If `reportFile` string is not empty then also save human-readable report.
2357        Shows some statistical data of closed positions.
2358
2359        :param start: see docstring in `GetDatesAsString()` method
2360        :param end: see docstring in `GetDatesAsString()` method
2361        :param show: if `True` then also prints all records to the console.
2362        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2363        :return: original list of dictionaries with history of deals records from API ("operations" key):
2364                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2365                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2366        """
2367        if self.accountId is None or not self.accountId:
2368            uLogger.error("Variable `accountId` must be defined for using this method!")
2369            raise Exception("Account ID required")
2370
2371        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2372
2373        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2374
2375        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2376        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2377        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2378        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2379        customStat = {}  # custom statistics in additional to responseJSON
2380
2381        # --- output report in human-readable format:
2382        if show or self.reportFile:
2383            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2384            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2385            nextDay = ""
2386
2387            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2388
2389            if len(ops) > 0:
2390                customStat = {
2391                    "opsCount": 0,  # total operations count
2392                    "buyCount": 0,  # buy operations
2393                    "sellCount": 0,  # sell operations
2394                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2395                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2396                    "payIn": {"rub": 0.},  # Deposit brokerage account
2397                    "payOut": {"rub": 0.},  # Withdrawals
2398                    "divs": {"rub": 0.},  # Dividends income
2399                    "coupons": {"rub": 0.},  # Coupon's income
2400                    "brokerCom": {"rub": 0.},  # Service commissions
2401                    "serviceCom": {"rub": 0.},  # Service commissions
2402                    "marginCom": {"rub": 0.},  # Margin commissions
2403                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2404                }
2405
2406                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2407                for item in ops:
2408                    if item["state"] == "OPERATION_STATE_EXECUTED":
2409                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2410
2411                        # count buy operations:
2412                        if "_BUY" in item["operationType"]:
2413                            customStat["buyCount"] += 1
2414
2415                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2416                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2417
2418                            else:
2419                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2420
2421                        # count sell operations:
2422                        elif "_SELL" in item["operationType"]:
2423                            customStat["sellCount"] += 1
2424
2425                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2426                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2427
2428                            else:
2429                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2430
2431                        # count incoming operations:
2432                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2433                            if item["payment"]["currency"] in customStat["payIn"].keys():
2434                                customStat["payIn"][item["payment"]["currency"]] += payment
2435
2436                            else:
2437                                customStat["payIn"][item["payment"]["currency"]] = payment
2438
2439                        # count withdrawals operations:
2440                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2441                            if item["payment"]["currency"] in customStat["payOut"].keys():
2442                                customStat["payOut"][item["payment"]["currency"]] += payment
2443
2444                            else:
2445                                customStat["payOut"][item["payment"]["currency"]] = payment
2446
2447                        # count dividends income:
2448                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2449                            if item["payment"]["currency"] in customStat["divs"].keys():
2450                                customStat["divs"][item["payment"]["currency"]] += payment
2451
2452                            else:
2453                                customStat["divs"][item["payment"]["currency"]] = payment
2454
2455                        # count coupon's income:
2456                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2457                            if item["payment"]["currency"] in customStat["coupons"].keys():
2458                                customStat["coupons"][item["payment"]["currency"]] += payment
2459
2460                            else:
2461                                customStat["coupons"][item["payment"]["currency"]] = payment
2462
2463                        # count broker commissions:
2464                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2465                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2466                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2467
2468                            else:
2469                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2470
2471                        # count service commissions:
2472                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2473                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2474                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2475
2476                            else:
2477                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2478
2479                        # count margin commissions:
2480                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2481                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2482                                customStat["marginCom"][item["payment"]["currency"]] += payment
2483
2484                            else:
2485                                customStat["marginCom"][item["payment"]["currency"]] = payment
2486
2487                        # count withholding taxes:
2488                        elif "_TAX" in item["operationType"]:
2489                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2490                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2491
2492                            else:
2493                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2494
2495                        else:
2496                            continue
2497
2498                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2499
2500                # --- view "Actions" lines:
2501                info.extend([
2502                    "| 1                          | 2                             | 3                            | 4                    | 5                      |\n",
2503                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2504                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2505                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2506                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2507                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2508                    ),
2509                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2510                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2511                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2512                    ),
2513                ])
2514
2515                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2516                for key in opsKeys:
2517                    if key == "rub":
2518                        continue
2519
2520                    info.extend([
2521                        "|                            |                               | {:<28} |                      |                        |\n".format(
2522                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2523                        ),
2524                        "|                            |                               | {:<28} |                      |                        |\n".format(
2525                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2526                        ),
2527                    ])
2528
2529                info.append(splitLine1)
2530
2531                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2532                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2533                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2534                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2535                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2536                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2537                    )
2538
2539                # --- view "Payments" lines:
2540                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2541                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2542
2543                for key in paymentsKeys:
2544                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2545
2546                info.append(splitLine1)
2547
2548                # --- view "Commissions and taxes" lines:
2549                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2550                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2551
2552                for key in comKeys:
2553                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2554
2555                info.append(splitLine1)
2556
2557                info.extend([
2558                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2559                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2560                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2561                ])
2562
2563            else:
2564                info.append("Broker returned no operations during this period\n")
2565
2566            # --- view "Operations" section:
2567            for item in ops:
2568                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2569                    continue
2570
2571                else:
2572                    self.figi = item["figi"] if item["figi"] else ""
2573                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2574                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2575
2576                    # group of deals during one day:
2577                    if nextDay and item["date"].split("T")[0] != nextDay:
2578                        info.append(splitLine2)
2579                        nextDay = ""
2580
2581                    else:
2582                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2583
2584                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2585                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2586                        self.figi if self.figi else "—",
2587                        instrument["ticker"] if instrument else "—",
2588                        instrument["type"] if instrument else "—",
2589                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2590                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2591                        TKS_OPERATION_STATES[item["state"]],
2592                        TKS_OPERATION_TYPES[item["operationType"]],
2593                    ))
2594
2595            infoText = "".join(info)
2596
2597            if show:
2598                uLogger.info(infoText)
2599
2600            if self.reportFile:
2601                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2602                    fH.write(infoText)
2603
2604                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2605
2606        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in GetDatesAsString() method
  • end: see docstring in GetDatesAsString() method
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2608    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2609        """
2610        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2611
2612        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2613        Warning! Broker server used ISO UTC time by default.
2614
2615        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2616        Also, `historyFile` used to update history with `onlyMissing` parameter.
2617
2618        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2619
2620        :param start: see docstring in `GetDatesAsString()` method.
2621        :param end: see docstring in `GetDatesAsString()` method.
2622        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2623                         `"hour"`, `"day"`. Default: `"hour"`.
2624        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2625                            False by default. Warning! History appends only from last candle to current time
2626                            with always update last candle!
2627        :param csvSep: separator if csv-file is used, `,` by default.
2628        :param show: if `True` then also prints Pandas DataFrame to the console.
2629        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2630                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2631        """
2632        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2633        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2634        history = None  # empty pandas object for history
2635
2636        if interval not in TKS_CANDLE_INTERVALS.keys():
2637            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2638            raise Exception("Incorrect value")
2639
2640        if not (self.ticker or self.figi):
2641            uLogger.error("Ticker or FIGI must be defined!")
2642            raise Exception("Ticker or FIGI required")
2643
2644        if self.ticker and not self.figi:
2645            instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False)
2646            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2647
2648        if self.figi and not self.ticker:
2649            instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False)
2650            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2651
2652        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2653        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2654        if interval.lower() != "day":
2655            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2656
2657        delta = dtEnd - dtStart  # current UTC time minus last time in file
2658        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2659
2660        # calculate history length in candles:
2661        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2662        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2663            length += 1  # to avoid fraction time
2664
2665        # calculate data blocks count:
2666        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2667
2668        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2669        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2670        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2671        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2672        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2673
2674        tempOld = None  # pandas object for old history, if --only-missing key present
2675        lastTime = None  # datetime object of last old candle in file
2676
2677        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2678            uLogger.debug("--only-missing key present, add only last missing candles...")
2679            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2680
2681            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2682
2683            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2684            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2685            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2686            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2687
2688            # get last datetime object from last string in file or minus 1 delta if file is empty:
2689            if len(tempOld) > 0:
2690                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2691
2692            else:
2693                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2694
2695            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2696
2697        responseJSONs = []  # raw history blocks of data
2698
2699        blockEnd = dtEnd
2700        for item in range(blocks):
2701            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2702            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2703
2704            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2705                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2706            ))
2707
2708            if blockStart == blockEnd:
2709                uLogger.debug("Skipped this zero-length block...")
2710
2711            else:
2712                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2713                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2714                self.body = str({
2715                    "figi": self.figi,
2716                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2717                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2718                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2719                })
2720                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False)
2721
2722                if "code" in responseJSON.keys():
2723                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2724
2725                else:
2726                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2727                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2728
2729                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2730
2731            blockEnd = blockStart
2732
2733        printCount = len(responseJSONs)  # candles to show in console
2734        if responseJSONs:
2735            tempHistory = pd.DataFrame(
2736                data={
2737                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2738                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2739                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2740                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2741                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2742                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2743                    "volume": [int(item["volume"]) for item in responseJSONs],
2744                },
2745                index=range(len(responseJSONs)),
2746                columns=["date", "time", "open", "high", "low", "close", "volume"],
2747            )
2748            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2749            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2750
2751            # append only newest candles to old history if --only-missing key present:
2752            if onlyMissing and tempOld is not None and lastTime is not None:
2753                index = 0  # find start index in tempHistory data:
2754
2755                for i, item in tempHistory.iterrows():
2756                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2757
2758                    if curTime == lastTime:
2759                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2760                        index = i
2761                        printCount = index + 1
2762                        break
2763
2764                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2765
2766            else:
2767                history = tempHistory  # if no `--only-missing` key then load full data from server
2768
2769            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2770
2771        if history is not None and not history.empty:
2772            if show:
2773                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2774                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2775                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2776                ))
2777
2778        else:
2779            uLogger.warning("Received an empty candles history!")
2780
2781        if self.historyFile is not None:
2782            if history is not None and not history.empty:
2783                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2784                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2785
2786            else:
2787                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2788
2789        else:
2790            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2791
2792        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in GetDatesAsString() method.
  • end: see docstring in GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2794    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2795        """
2796        Load candles history from csv-file and return Pandas DataFrame object.
2797
2798        See also: `History()` and `ShowHistoryChart()` methods.
2799
2800        :param filePath: path to csv-file to open.
2801        """
2802        loadedHistory = None  # init candles data object
2803
2804        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2805
2806        if os.path.exists(filePath):
2807            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2808
2809            tfStr = self.priceModel.FormattedDelta(
2810                self.priceModel.timeframe,
2811                "{days} days {hours}h {minutes}m {seconds}s",
2812            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2813                self.priceModel.timeframe,
2814                "{hours}h {minutes}m {seconds}s",
2815            )
2816
2817            if loadedHistory is not None and not loadedHistory.empty:
2818                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2819                    len(loadedHistory),
2820                    tfStr,
2821                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2822                )
2823
2824            else:
2825                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2826
2827        else:
2828            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2829
2830        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2832    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2833        """
2834        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2835
2836        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2837        Default: `index.html` (both for interact and non-interact candlesticks chart).
2838
2839        See also: `History()` and `LoadHistory()` methods.
2840
2841        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2842        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2843                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2844                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2845                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2846        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2847                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2848        """
2849        if isinstance(candles, str):
2850            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2851            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2852
2853        elif isinstance(candles, pd.DataFrame):
2854            self.priceModel.prices = candles  # set candles chain from variable
2855            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2856
2857            if "datetime" not in candles.columns:
2858                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2859
2860        else:
2861            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2862            raise Exception("Incorrect value")
2863
2864        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2865
2866        if interact:
2867            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2868
2869            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2870
2871        else:
2872            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2873
2874            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2875
2876        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2878    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2879        """
2880        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2881        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2882
2883        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2884
2885        :param operation: string "Buy" or "Sell".
2886        :param lots: volume, integer count of lots >= 1.
2887        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2888        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2889        :param expDate: string "Undefined" by default or local date in future,
2890                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2891        :return: JSON with response from broker server.
2892        """
2893        if self.accountId is None or not self.accountId:
2894            uLogger.error("Variable `accountId` must be defined for using this method!")
2895            raise Exception("Account ID required")
2896
2897        if operation is None or not operation or operation not in ("Buy", "Sell"):
2898            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2899            raise Exception("Incorrect value")
2900
2901        if lots is None or lots < 1:
2902            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2903            lots = 1
2904
2905        if tp is None or tp < 0:
2906            tp = 0
2907
2908        if sl is None or sl < 0:
2909            sl = 0
2910
2911        if expDate is None or not expDate:
2912            expDate = "Undefined"
2913
2914        if not (self.ticker or self.figi):
2915            uLogger.error("Ticker or FIGI must be defined!")
2916            raise Exception("Ticker or FIGI required")
2917
2918        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
2919        self.ticker = instrument["ticker"]
2920        self.figi = instrument["figi"]
2921
2922        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2923
2924        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2925        self.body = str({
2926            "figi": self.figi,
2927            "quantity": str(lots),
2928            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2929            "accountId": str(self.accountId),
2930            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2931        })
2932        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False)
2933
2934        if "orderId" in response.keys():
2935            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2936                operation, response["orderId"],
2937                self.ticker, self.figi, lots,
2938                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2939                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2940                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2941            ))
2942
2943        else:
2944            uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.")
2945
2946        if tp > 0:
2947            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2948
2949        if sl > 0:
2950            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2951
2952        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2954    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2955        """
2956        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2957        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2958
2959        See also: `Order()` and `Trade()` docstrings.
2960
2961        :param lots: volume, integer count of lots >= 1.
2962        :param tp: float > 0, take profit price of stop-order.
2963        :param sl: float > 0, stop loss price of stop-order.
2964        :param expDate: it's a local date in future.
2965                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2966        :return: JSON with response from broker server.
2967        """
2968        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2970    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2971        """
2972        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2973        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2974
2975        See also: `Order()` and `Trade()` docstrings.
2976
2977        :param lots: volume, integer count of lots >= 1.
2978        :param tp: float > 0, take profit price of stop-order.
2979        :param sl: float > 0, stop loss price of stop-order.
2980        :param expDate: it's a local date in the future.
2981                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2982        :return: JSON with response from broker server.
2983        """
2984        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2986    def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2987        """
2988        Close position of given instruments.
2989
2990        :param tickers: tickers list of instruments that must be closed.
2991        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2992                         This avoids unnecessary downloading data from the server.
2993        """
2994        if not tickers:
2995            uLogger.info("Tickers list is empty, nothing to close.")
2996
2997        else:
2998            if portfolio is None or not portfolio:
2999                portfolio = self.Overview(show=False)
3000
3001            allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3002            uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers))
3003
3004            for ticker in tickers:
3005                if ticker not in allOpenedTickers:
3006                    uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker))
3007                    continue
3008
3009                # search open trade info about instrument by ticker:
3010                instrument = {}
3011                for iType in TKS_INSTRUMENTS:
3012                    if instrument:
3013                        break
3014
3015                    for item in portfolio["stat"][iType]:
3016                        if item["ticker"] == ticker:
3017                            instrument = item
3018                            break
3019
3020                if instrument:
3021                    self.ticker = ticker
3022                    self.figi = instrument["figi"]
3023
3024                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3025                        self.ticker,
3026                        self.figi,
3027                        int(instrument["volume"]),
3028                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3029                    ))
3030
3031                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3032
3033                    if tradeLots > 0:
3034                        if instrument["blocked"] > 0:
3035                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3036                                instrument["blocked"],
3037                                self.ticker,
3038                                tradeLots,
3039                            ))
3040
3041                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3042                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3043
3044                    else:
3045                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))

Close position of given instruments.

Parameters
  • tickers: tickers list of instruments that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3047    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3048        """
3049        Close all positions of given instruments with defined type.
3050
3051        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3052        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3053                         This avoids unnecessary downloading data from the server.
3054        """
3055        if iType not in TKS_INSTRUMENTS:
3056            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3057
3058        else:
3059            if portfolio is None or not portfolio:
3060                portfolio = self.Overview(show=False)
3061
3062            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3063            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3064
3065            if tickers and portfolio:
3066                self.CloseTrades(tickers, portfolio)
3067
3068            else:
3069                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3071    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3072        """
3073        Universal method to create market or limit orders with all available parameters for current `accountId`.
3074        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3075
3076        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3077        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3078
3079        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3080        then broker immediately open market order as you can do simple --buy or --sell operations!
3081
3082        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3083        When current price will go up or down to target price value then broker opens a limit order.
3084        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3085
3086        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3087
3088        :param operation: string "Buy" or "Sell".
3089        :param orderType: string "Limit" or "Stop".
3090        :param lots: volume, integer count of lots >= 1.
3091        :param targetPrice: target price > 0. This is open trade price for limit order.
3092        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3093                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3094        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3095                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3096                         Stop loss order always executed by market price.
3097        :param expDate: string "Undefined" by default or local date in future.
3098                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3099                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3100                        A limit order has no expiration date, it lasts until the end of the trading day.
3101        :return: JSON with response from broker server.
3102        """
3103        if self.accountId is None or not self.accountId:
3104            uLogger.error("Variable `accountId` must be defined for using this method!")
3105            raise Exception("Account ID required")
3106
3107        if operation is None or not operation or operation not in ("Buy", "Sell"):
3108            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3109            raise Exception("Incorrect value")
3110
3111        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3112            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3113            raise Exception("Incorrect value")
3114
3115        if lots is None or lots < 1:
3116            uLogger.error("You must define trade volume > 0: integer count of lots!")
3117            raise Exception("Incorrect value")
3118
3119        if targetPrice is None or targetPrice <= 0:
3120            uLogger.error("Target price for limit-order must be greater than 0!")
3121            raise Exception("Incorrect value")
3122
3123        if limitPrice is None or limitPrice <= 0:
3124            limitPrice = targetPrice
3125
3126        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3127            stopType = "Limit"
3128
3129        if expDate is None or not expDate:
3130            expDate = "Undefined"
3131
3132        if not (self.ticker or self.figi):
3133            uLogger.error("Tocker or FIGI must be defined!")
3134            raise Exception("Ticker or FIGI required")
3135
3136        response = {}
3137        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
3138        self.ticker = instrument["ticker"]
3139        self.figi = instrument["figi"]
3140
3141        if orderType == "Limit":
3142            uLogger.debug(
3143                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3144                    self.ticker, self.figi,
3145                    operation, lots, targetPrice, instrument["currency"],
3146                ))
3147
3148            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3149            self.body = str({
3150                "figi": self.figi,
3151                "quantity": str(lots),
3152                "price": FloatToNano(targetPrice),
3153                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3154                "accountId": str(self.accountId),
3155                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3156            })
3157            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3158
3159            if "orderId" in response.keys():
3160                uLogger.info(
3161                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3162                        response["orderId"],
3163                        self.ticker, self.figi,
3164                        operation, lots, targetPrice, instrument["currency"],
3165                    ))
3166
3167                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3168                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3169                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3170                            targetPrice, instrument["currency"],
3171                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3172                        ))
3173
3174                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3175                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3176                            targetPrice, instrument["currency"],
3177                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3178                        ))
3179
3180            else:
3181                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3182
3183        if orderType == "Stop":
3184            uLogger.debug(
3185                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3186                    self.ticker, self.figi,
3187                    operation, lots,
3188                    targetPrice, instrument["currency"],
3189                    limitPrice, instrument["currency"],
3190                    stopType, expDate,
3191                ))
3192
3193            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3194            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3195            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3196
3197            body = {
3198                "figi": self.figi,
3199                "quantity": str(lots),
3200                "price": FloatToNano(limitPrice),
3201                "stopPrice": FloatToNano(targetPrice),
3202                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3203                "accountId": str(self.accountId),
3204                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3205                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3206            }
3207
3208            if expDateUTC:
3209                body["expireDate"] = expDateUTC
3210
3211            self.body = str(body)
3212            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3213
3214            if "stopOrderId" in response.keys():
3215                uLogger.info(
3216                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3217                        response["stopOrderId"],
3218                        self.ticker, self.figi,
3219                        operation, lots,
3220                        targetPrice, instrument["currency"],
3221                        limitPrice, instrument["currency"],
3222                        TKS_STOP_ORDER_TYPES[stopOrderType],
3223                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3224                    ))
3225
3226                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3227                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3228                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3229                            targetPrice, instrument["currency"],
3230                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3231                        ))
3232
3233                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3234                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3235                            targetPrice, instrument["currency"],
3236                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3237                        ))
3238
3239            else:
3240                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3241
3242        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3244    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3245        """
3246        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3247        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3248        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3249        See also: `Order()` docstring.
3250
3251        :param lots: volume, integer count of lots >= 1.
3252        :param targetPrice: target price > 0. This is open trade price for limit order.
3253        :return: JSON with response from broker server.
3254        """
3255        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3257    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3258        """
3259        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3260        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3261        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3262        target price value then broker opens a limit order. See also: `Order()` docstring.
3263
3264        :param lots: volume, integer count of lots >= 1.
3265        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3266        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3267                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3268        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3269                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3270        :param expDate: string "Undefined" by default or local date in future.
3271                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3272                        This date is converting to UTC format for server.
3273        :return: JSON with response from broker server.
3274        """
3275        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3277    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3278        """
3279        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3280        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3281        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3282        See also: `Order()` docstring.
3283
3284        :param lots: volume, integer count of lots >= 1.
3285        :param targetPrice: target price > 0. This is open trade price for limit order.
3286        :return: JSON with response from broker server.
3287        """
3288        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3290    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3291        """
3292        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3293        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3294        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3295        target price value then broker opens a limit order. See also: `Order()` docstring.
3296
3297        :param lots: volume, integer count of lots >= 1.
3298        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3299        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3300                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3301        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3302                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3303        :param expDate: string "Undefined" by default or local date in future.
3304                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3305                        This date is converting to UTC format for server.
3306        :return: JSON with response from broker server.
3307        """
3308        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3310    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3311        """
3312        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3313
3314        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3315        :param allOrdersIDs: pre-received lists of all active pending orders.
3316                             This avoids unnecessary downloading data from the server.
3317        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3318        """
3319        if self.accountId is None or not self.accountId:
3320            uLogger.error("Variable `accountId` must be defined for using this method!")
3321            raise Exception("Account ID required")
3322
3323        if orderIDs:
3324            if allOrdersIDs is None or not allOrdersIDs:
3325                rawOrders = self.RequestPendingOrders()
3326                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3327
3328            if allStopOrdersIDs is None or not allStopOrdersIDs:
3329                rawStopOrders = self.RequestStopOrders()
3330                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3331
3332            for orderID in orderIDs:
3333                idInPendingOrders = orderID in allOrdersIDs
3334                idInStopOrders = orderID in allStopOrdersIDs
3335
3336                if not (idInPendingOrders or idInStopOrders):
3337                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3338                    continue
3339
3340                else:
3341                    if idInPendingOrders:
3342                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3343
3344                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3345                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3346                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3347                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3348
3349                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3350                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3351                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3352
3353                        else:
3354                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3355
3356                    elif idInStopOrders:
3357                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3358
3359                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3360                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3361                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3362                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3363
3364                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3365                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3366                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3367
3368                        else:
3369                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3370
3371                    else:
3372                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3374    def CloseAllOrders(self) -> None:
3375        """
3376        Gets a list of open pending and stop orders and cancel it all.
3377        """
3378        rawOrders = self.RequestPendingOrders()
3379        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3380        lenOrders = len(allOrdersIDs)
3381
3382        rawStopOrders = self.RequestStopOrders()
3383        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3384        lenSOrders = len(allStopOrdersIDs)
3385
3386        if lenOrders > 0 or lenSOrders > 0:
3387            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3388
3389            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3390
3391        else:
3392            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3394    def CloseAll(self, *args) -> None:
3395        """
3396        Close all available (not blocked) opened trades and orders.
3397
3398        Also, you can select one or more keywords case-insensitive:
3399        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3400
3401        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3402        """
3403        overview = self.Overview(show=False)  # get all open trades info
3404
3405        if len(args) == 0:
3406            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3407            self.CloseAllOrders()  # close all pending and stop orders
3408
3409            for iType in TKS_INSTRUMENTS:
3410                if iType != "Currencies":
3411                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3412
3413        else:
3414            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3415            lowerArgs = [x.lower() for x in args]
3416
3417            if "orders" in lowerArgs:
3418                self.CloseAllOrders()  # close all pending and stop orders
3419
3420            for iType in TKS_INSTRUMENTS:
3421                if iType.lower() in lowerArgs and iType != "Currencies":
3422                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

@staticmethod
def ParseOrderParameters(operation, **inputParameters)
3424    @staticmethod
3425    def ParseOrderParameters(operation, **inputParameters):
3426        """
3427        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3428
3429        :param operation: string "Buy" or "Sell".
3430        :param inputParameters: this is dict of strings that looks like this
3431               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3432               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3433               "prices" key: one or more prices to open limit-orders
3434               Counts of values in lots and prices lists must be equals!
3435        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3436        """
3437        # TODO: update order grid work with api v2
3438        pass
3439        # uLogger.debug("Input parameters: {}".format(inputParameters))
3440        #
3441        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3442        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3443        #     raise Exception("Incorrect value")
3444        #
3445        # if "l" in inputParameters.keys():
3446        #     inputParameters["lots"] = inputParameters.pop("l")
3447        #
3448        # if "p" in inputParameters.keys():
3449        #     inputParameters["prices"] = inputParameters.pop("p")
3450        #
3451        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3452        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3453        #     raise Exception("Incorrect value")
3454        #
3455        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3456        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3457        #
3458        # if len(lots) != len(prices):
3459        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3460        #     raise Exception("Incorrect value")
3461        #
3462        # uLogger.debug("Extracted parameters for orders:")
3463        # uLogger.debug("lots = {}".format(lots))
3464        # uLogger.debug("prices = {}".format(prices))
3465        #
3466        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3467        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3468        # uLogger.debug("Order parameters: {}".format(result))
3469        #
3470        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3472    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3473        """
3474        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3475
3476        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3477        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3478        """
3479        result = False
3480        msg = "Instrument not defined!"
3481
3482        if portfolio is None or not portfolio:
3483            portfolio = self.Overview(show=False)
3484
3485        if self.ticker:
3486            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3487            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3488
3489            for iType in TKS_INSTRUMENTS:
3490                for instrument in portfolio["stat"][iType]:
3491                    if instrument["ticker"] == self.ticker:
3492                        result = True
3493                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3494                        break
3495
3496        elif self.figi:
3497            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3498            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3499
3500            for iType in TKS_INSTRUMENTS:
3501                for instrument in portfolio["stat"][iType]:
3502                    if instrument["figi"] == self.figi:
3503                        result = True
3504                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3505                        break
3506
3507        else:
3508            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3509
3510        uLogger.debug(msg)
3511
3512        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3514    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3515        """
3516        Returns instrument is in the user's portfolio if it presents there.
3517        Instrument must be defined by `ticker` (highly priority) or `figi`.
3518
3519        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3520        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3521        """
3522        result = None
3523        msg = "Instrument not defined!"
3524
3525        if portfolio is None or not portfolio:
3526            portfolio = self.Overview(show=False)
3527
3528        if self.ticker:
3529            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3530            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3531
3532            for iType in TKS_INSTRUMENTS:
3533                for instrument in portfolio["stat"][iType]:
3534                    if instrument["ticker"] == self.ticker:
3535                        result = instrument
3536                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3537                        break
3538
3539        elif self.figi:
3540            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3541            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3542
3543            for iType in TKS_INSTRUMENTS:
3544                for instrument in portfolio["stat"][iType]:
3545                    if instrument["figi"] == self.figi:
3546                        result = instrument
3547                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3548                        break
3549
3550        else:
3551            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3552
3553        uLogger.debug(msg)
3554
3555        return result

Returns instrument is in the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def RequestLimits(self) -> dict:
3557    def RequestLimits(self) -> dict:
3558        """
3559        Method for obtaining the available funds for withdrawal for current `accountId`.
3560
3561        See also:
3562        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3563        - `OverviewLimits()` method
3564
3565        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3566                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3567                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3568                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3569        """
3570        if self.accountId is None or not self.accountId:
3571            uLogger.error("Variable `accountId` must be defined for using this method!")
3572            raise Exception("Account ID required")
3573
3574        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3575
3576        self.body = str({"accountId": self.accountId})
3577        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3578        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3579
3580        uLogger.debug("Records about available funds for withdrawal successfully received")
3581
3582        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3584    def OverviewLimits(self, show: bool = False) -> dict:
3585        """
3586        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3587
3588        See also: `RequestLimits()`.
3589
3590        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3591        :return: dict with raw parsed data from server and some calculated statistics about it.
3592        """
3593        if self.accountId is None or not self.accountId:
3594            uLogger.error("Variable `accountId` must be defined for using this method!")
3595            raise Exception("Account ID required")
3596
3597        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3598
3599        view = {
3600            "rawLimits": rawLimits,
3601            "limits": {  # parsed data for every currency:
3602                "money": {  # this is an array of portfolio currency positions
3603                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3604                },
3605                "blocked": {  # this is an array of blocked currency
3606                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3607                },
3608                "blockedGuarantee": {  # this is locked money under collateral for futures
3609                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3610                },
3611            },
3612        }
3613
3614        # --- Prepare text table with limits in human-readable format:
3615        if show:
3616            info = [
3617                "# Withdrawal limits\n\n",
3618                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3619                "* **Account ID:** [{}]\n".format(self.accountId),
3620                "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3621                "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3622            ]
3623
3624            for curr in view["limits"]["money"].keys():
3625                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3626                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3627                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3628
3629                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3630                    "[{}]".format(curr),
3631                    "{:.2f}".format(view["limits"]["money"][curr]),
3632                    "{:.2f}".format(availableMoney),
3633                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3634                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3635                )
3636
3637                if curr == "rub":
3638                    info.insert(5, infoStr)  # insert at first position in table and after headers
3639
3640                else:
3641                    info.append(infoStr)
3642
3643            infoText = "".join(info)
3644
3645            uLogger.info(infoText)
3646
3647            if self.withdrawalLimitsFile:
3648                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3649                    fH.write(infoText)
3650
3651                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3652
3653        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3655    def RequestAccounts(self) -> dict:
3656        """
3657        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3658
3659        See also:
3660        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3661        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3662        - `OverviewUserInfo()` method
3663
3664        :return: dict with raw data from server that contains accounts info. Example of dict:
3665                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3666                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3667                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3668                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3669        """
3670        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3671
3672        self.body = str({})
3673        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3674        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3675
3676        uLogger.debug("Records about available accounts successfully received")
3677
3678        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
3680    def RequestUserInfo(self) -> dict:
3681        """
3682        Method for requesting common user's information.
3683
3684        See also:
3685        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3686        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3687        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3688        - `OverviewUserInfo()` method
3689
3690        :return: dict with raw data from server that contains user's information. Example of dict:
3691                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3692                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3693        """
3694        uLogger.debug("Requesting common user's information. Wait, please...")
3695
3696        self.body = str({})
3697        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3698        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3699
3700        uLogger.debug("Records about current user successfully received")
3701
3702        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
3704    def RequestMarginStatus(self, accountId: str = None) -> dict:
3705        """
3706        Method for requesting margin calculation for defined account ID.
3707
3708        See also:
3709        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3710        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3711        - `OverviewUserInfo()` method
3712
3713        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3714        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3715                 Example of responses:
3716                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3717                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3718                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3719                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3720                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3721                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3722        """
3723        if accountId is None or not accountId:
3724            if self.accountId is None or not self.accountId:
3725                uLogger.error("Variable `accountId` must be defined for using this method!")
3726                raise Exception("Account ID required")
3727
3728            else:
3729                accountId = self.accountId  # use `self.accountId` (main ID) by default
3730
3731        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3732
3733        self.body = str({"accountId": accountId})
3734        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3735        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3736
3737        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3738            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3739            rawMargin = {}
3740
3741        else:
3742            uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3743
3744        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
3746    def RequestTariffLimits(self) -> dict:
3747        """
3748        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3749
3750        See also:
3751        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3752        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3753        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3754        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3755        - `OverviewUserInfo()` method
3756
3757        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3758                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3759                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3760        """
3761        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3762
3763        self.body = str({})
3764        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3765        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3766
3767        uLogger.debug("Records with limits of current tariff successfully received")
3768
3769        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
3771    def RequestBondCoupons(self, iJSON: dict) -> dict:
3772        """
3773        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3774        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3775        All dates are in UTC timezone.
3776
3777        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3778        Documentation:
3779        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3780        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3781
3782        See also: `ExtendBondsData()`.
3783
3784        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3785                      If raw iJSON is not data of bond then server returns an error [400] with message:
3786                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3787        :return: dictionary with bond payment calendar. Response example
3788                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3789                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3790                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3791                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3792        """
3793        if iJSON["figi"] is None or not iJSON["figi"]:
3794            uLogger.error("FIGI must be defined for using this method!")
3795            raise Exception("FIGI required")
3796
3797        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3798        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3799
3800        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3801            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3802            self.figi,
3803            startDate,
3804            endDate,
3805        ))
3806
3807        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3808        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3809        calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False)
3810
3811        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3812            uLogger.warning("Instrument type is not bond!")
3813
3814        else:
3815            uLogger.debug("Records about bond payment calendar successfully received")
3816
3817        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self.ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
3819    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3820        """
3821        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3822        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3823        coupon yields, current yields and some statistics etc.
3824
3825        WARNING! This is too long operation if a lot of bonds requested from broker server.
3826
3827        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3828
3829        :param instruments: list of strings with tickers or FIGIs.
3830        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3831                     for further used by data scientists or stock analytics.
3832        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3833                 In XLSX-file and Pandas DataFrame fields mean:
3834                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3835                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3836        """
3837        if instruments is None or not instruments:
3838            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3839            raise Exception("Ticker or FIGI required")
3840
3841        if isinstance(instruments, str):
3842            instruments = [instruments]
3843
3844        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3845
3846        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3847
3848        iCount = len(uniqueInstruments)
3849        tooLong = iCount >= 20
3850        if tooLong:
3851            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3852
3853        bonds = None
3854        for i, self.figi in enumerate(uniqueInstruments):
3855            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3856
3857            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3858                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3859                rawBond = self.SearchByFIGI(requestPrice=True)
3860
3861                # Widen raw data with UTC current time (iData["actualDateTime"]):
3862                actualDate = datetime.now(tzutc())
3863                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3864
3865                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3866                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3867
3868                # Replace some values with human-readable:
3869                iData["nominalCurrency"] = iData["nominal"]["currency"]
3870                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3871                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3872                iData["aciCurrency"] = iData["aciValue"]["currency"]
3873                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3874                iData["issueSize"] = int(iData["issueSize"])
3875                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3876                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3877                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3878                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3879                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3880                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3881                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3882                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3883                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3884                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3885
3886                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3887                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3888                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3889                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3890                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3891                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3892                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3893                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3894                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3895                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3896                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3897
3898                # Widen raw data with calendar data from `rawCalendar` values:
3899                calendarData = []
3900                for item in iData["rawCalendar"]["events"]:
3901                    calendarData.append({
3902                        "couponDate": item["couponDate"],
3903                        "couponNumber": int(item["couponNumber"]),
3904                        "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3905                        "payCurrency": item["payOneBond"]["currency"],
3906                        "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3907                        "couponType": TKS_COUPON_TYPES[item["couponType"]],
3908                        "couponStartDate": item["couponStartDate"],
3909                        "couponEndDate": item["couponEndDate"],
3910                        "couponPeriod": item["couponPeriod"],
3911                    })
3912
3913                # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3914                if "maturityDate" not in iData.keys():
3915                    iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3916
3917                # Widen raw data with Coupon Rate.
3918                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3919                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3920                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3921                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3922
3923                # Widen raw data with Yield to Maturity (YTM) on current date.
3924                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3925                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3926                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3927                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3928                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3929                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3930
3931                iData["calendar"] = calendarData  # adds calendar at the end
3932
3933                # Remove not used data:
3934                iData.pop("uid")
3935                iData.pop("positionUid")
3936                iData.pop("currentPrice")
3937                iData.pop("rawCalendar")
3938
3939                colNames = list(iData.keys())
3940                if bonds is None:
3941                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3942
3943                else:
3944                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3945
3946            else:
3947                uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"]))
3948
3949            processed = round(100 * (i + 1) / iCount, 1)
3950            if tooLong and processed % 5 == 0:
3951                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3952
3953            else:
3954                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3955
3956        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3957
3958        # Saving bonds from Pandas DataFrame to XLSX sheet:
3959        if xlsx and self.bondsXLSXFile:
3960            with pd.ExcelWriter(
3961                    path=self.bondsXLSXFile,
3962                    date_format=TKS_DATE_FORMAT,
3963                    datetime_format=TKS_DATE_TIME_FORMAT,
3964                    mode="w",
3965            ) as writer:
3966                bonds.to_excel(
3967                    writer,
3968                    sheet_name="Extended bonds data",
3969                    index=True,
3970                    encoding="UTF-8",
3971                    freeze_panes=(1, 1),
3972                )  # saving as XLSX-file with freeze first row and column as headers
3973
3974            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3975
3976        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
3978    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3979        """
3980        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
3981
3982        WARNING! This is too long operation if a lot of bonds requested from broker server.
3983
3984        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3985
3986        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
3987                        extended information about bonds: main info, current prices, bond payment calendar,
3988                        coupon yields, current yields and some statistics etc.
3989                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3990        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
3991                     for further used by data scientists or stock analytics.
3992        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
3993        """
3994        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3995            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3996
3997        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3998
3999        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4000        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4001        calendar = None
4002        for bond in extBonds.iterrows():
4003            for item in bond[1]["calendar"]:
4004                cData = {
4005                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4006                    "couponDate": item["couponDate"],
4007                    "figi": bond[1]["figi"],
4008                    "ticker": bond[1]["ticker"],
4009                    "name": bond[1]["name"],
4010                    "couponNumber": item["couponNumber"],
4011                    "payOneBond": item["payOneBond"],
4012                    "payCurrency": item["payCurrency"],
4013                    "couponType": item["couponType"],
4014                    "couponPeriod": item["couponPeriod"],
4015                    "fixDate": item["fixDate"],
4016                    "couponStartDate": item["couponStartDate"],
4017                    "couponEndDate": item["couponEndDate"],
4018                }
4019
4020                if calendar is None:
4021                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4022
4023                else:
4024                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4025
4026        calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4027
4028        # Saving calendar from Pandas DataFrame to XLSX sheet:
4029        if xlsx:
4030            xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4031
4032            with pd.ExcelWriter(
4033                    path=xlsxCalendarFile,
4034                    date_format=TKS_DATE_FORMAT,
4035                    datetime_format=TKS_DATE_TIME_FORMAT,
4036                    mode="w",
4037            ) as writer:
4038                humanReadable = calendar.copy(deep=True)
4039                humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4040                humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4041                humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4042                humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4043                humanReadable.columns = colNames  # human-readable column names
4044
4045                humanReadable.to_excel(
4046                    writer,
4047                    sheet_name="Bond payments calendar",
4048                    index=False,
4049                    encoding="UTF-8",
4050                    freeze_panes=(1, 2),
4051                )  # saving as XLSX-file with freeze first row and column as headers
4052
4053                del humanReadable  # release df in memory
4054
4055            uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4056
4057        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4059    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4060        """
4061        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4062        Also, creates Markdown file with calendar data, `calendar.md` by default.
4063
4064        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4065
4066        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4067                        extended information about bonds: main info, current prices, bond payment calendar,
4068                        coupon yields, current yields and some statistics etc.
4069                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4070        :param show: if `True` then also printing bonds payment calendar to the console,
4071                     otherwise save to file `calendarFile` only. `False` by default.
4072        :return: multilines text in Markdown format with bonds payment calendar as a table.
4073        """
4074        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4075            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4076
4077        infoText = "# Bond payments calendar\n\n"
4078
4079        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4080
4081        if not calendar.empty:
4082            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4083
4084            info = [
4085                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4086                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4087            ]
4088
4089            newMonth = False
4090            notOneBond = calendar["figi"].nunique() > 1
4091            for i, bond in enumerate(calendar.iterrows()):
4092                if newMonth and notOneBond:
4093                    info.append(splitLine)
4094
4095                info.append(
4096                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4097                        "  √" if bond[1]["paid"] else "  —",
4098                        bond[1]["couponDate"].split("T")[0],
4099                        bond[1]["figi"],
4100                        bond[1]["ticker"],
4101                        bond[1]["couponNumber"],
4102                        "{} {}".format(
4103                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4104                            bond[1]["payCurrency"],
4105                        ),
4106                        bond[1]["couponType"],
4107                        bond[1]["couponPeriod"],
4108                        bond[1]["fixDate"].split("T")[0],
4109                    )
4110                )
4111
4112                if i < len(calendar.values) - 1:
4113                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4114                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4115                    newMonth = False if curDate.month == nextDate.month else True
4116
4117                else:
4118                    newMonth = False
4119
4120            infoText += "".join(info)
4121
4122            if show:
4123                uLogger.info("{}".format(infoText))
4124
4125            if self.calendarFile is not None:
4126                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4127                    fH.write(infoText)
4128
4129                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4130
4131        else:
4132            infoText += "No data\n"
4133
4134        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4136    def OverviewAccounts(self, show: bool = False) -> dict:
4137        """
4138        Method for parsing and show simple table with all available user accounts.
4139
4140        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4141
4142        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4143        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4144                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4145                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4146                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4147                                                        "closed": "—", "access": "Full access" }, ...}}`
4148        """
4149        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4150
4151        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4152        accounts = {
4153            item["id"]: {
4154                "type": TKS_ACCOUNT_TYPES[item["type"]],
4155                "name": item["name"],
4156                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4157                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4158                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4159                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4160            } for item in rawAccounts["accounts"]
4161        }
4162
4163        # Raw and parsed data with some fields replaced in "stat" section:
4164        view = {
4165            "rawAccounts": rawAccounts,
4166            "stat": accounts,
4167        }
4168
4169        # --- Prepare simple text table with only accounts data in human-readable format:
4170        if show:
4171            info = [
4172                "# User accounts\n\n",
4173                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4174                "| Account ID   | Type                      | Status                    | Name                           |\n",
4175                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4176            ]
4177
4178            for account in view["stat"].keys():
4179                info.extend([
4180                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4181                        account,
4182                        view["stat"][account]["type"],
4183                        view["stat"][account]["status"],
4184                        view["stat"][account]["name"],
4185                    )
4186                ])
4187
4188            infoText = "".join(info)
4189
4190            uLogger.info(infoText)
4191
4192            if self.userAccountsFile:
4193                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4194                    fH.write(infoText)
4195
4196                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4197
4198        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4200    def OverviewUserInfo(self, show: bool = False) -> dict:
4201        """
4202        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4203
4204        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4205
4206        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4207        :return: dict with raw parsed data from server and some calculated statistics about it.
4208        """
4209        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4210        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4211        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4212        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4213        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4214        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4215
4216        # This is dict with parsed common user data:
4217        userInfo = {
4218            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4219            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4220            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4221            "tariff": rawUserInfo["tariff"],
4222        }
4223
4224        # This is an array of dict with parsed margin statuses for every account IDs:
4225        margins = {}
4226        for accountId in accounts.keys():
4227            if rawMargins[accountId]:
4228                margins[accountId] = {
4229                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4230                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4231                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4232                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4233                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4234                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4235                }
4236
4237            else:
4238                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4239
4240        unary = {}  # unary-connection limits
4241        for item in rawTariffLimits["unaryLimits"]:
4242            if item["limitPerMinute"] in unary.keys():
4243                unary[item["limitPerMinute"]].extend(item["methods"])
4244
4245            else:
4246                unary[item["limitPerMinute"]] = item["methods"]
4247
4248        stream = {}  # stream-connection limits
4249        for item in rawTariffLimits["streamLimits"]:
4250            if item["limit"] in stream.keys():
4251                stream[item["limit"]].extend(item["streams"])
4252
4253            else:
4254                stream[item["limit"]] = item["streams"]
4255
4256        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4257        limits = {
4258            "unary": unary,
4259            "stream": stream,
4260        }
4261
4262        # Raw and parsed data as an output result:
4263        view = {
4264            "rawUserInfo": rawUserInfo,
4265            "rawAccounts": rawAccounts,
4266            "rawMargins": rawMargins,
4267            "rawTariffLimits": rawTariffLimits,
4268            "stat": {
4269                "userInfo": userInfo,
4270                "accounts": accounts,
4271                "margins": margins,
4272                "limits": limits,
4273            },
4274        }
4275
4276        # --- Prepare text table with user information in human-readable format:
4277        if show:
4278            info = [
4279                "# Full user information\n\n",
4280                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4281                "## Common information\n\n",
4282                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4283                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4284                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4285                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4286                "\n## User accounts\n\n",
4287            ]
4288
4289            for account in view["stat"]["accounts"].keys():
4290                info.extend([
4291                    "### ID: [{}]\n\n".format(account),
4292                    "| Parameters           | Values                                                       |\n",
4293                    "|----------------------|--------------------------------------------------------------|\n",
4294                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4295                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4296                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4297                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4298                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4299                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4300                ])
4301
4302                if margins[account]:
4303                    info.extend([
4304                        "| Margin status:       | Enabled                                                      |\n",
4305                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4306                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4307                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4308                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4309                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4310                    ])
4311
4312                else:
4313                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4314
4315            info.extend([
4316                "\n## Current user tariff limits\n",
4317                "\nSee also:\n",
4318                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4319                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4320                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4321                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4322                "\n### Unary limits\n",
4323            ])
4324
4325            if unary:
4326                for key, values in sorted(unary.items()):
4327                    info.append("\n* Max requests per minute: {}\n".format(key))
4328
4329                    for value in values:
4330                        info.append("  - {}\n".format(value))
4331
4332            else:
4333                info.append("\nNot available\n")
4334
4335            info.append("\n### Stream limits\n")
4336
4337            if stream:
4338                for key, values in sorted(stream.items()):
4339                    info.append("\n* Max stream connections: {}\n".format(key))
4340
4341                    for value in values:
4342                        info.append("  - {}\n".format(value))
4343
4344            else:
4345                info.append("\nNot available\n")
4346
4347            infoText = "".join(info)
4348
4349            uLogger.info(infoText)
4350
4351            if self.userInfoFile:
4352                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4353                    fH.write(infoText)
4354
4355                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4356
4357        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4360class Args:
4361    """
4362    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4363    """
4364    def __init__(self, **kwargs):
4365        self.__dict__.update(kwargs)
4366
4367    def __getattr__(self, item):
4368        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4364    def __init__(self, **kwargs):
4365        self.__dict__.update(kwargs)
def ParseArgs()
4371def ParseArgs():
4372    """This function get and parse command line keys."""
4373    parser = ArgumentParser()  # command-line string parser
4374
4375    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4376    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4377
4378    # --- options:
4379
4380    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4381    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4382    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4383
4384    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4385    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4386
4387    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4388    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4389
4390    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4391
4392    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4393    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4394    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4395
4396    parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4397
4398    # --- commands:
4399
4400    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4401
4402    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4403    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4404    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4405    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4406    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4407    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4408    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4409    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4410
4411    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4412    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4413    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4414    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4415    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4416
4417    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4418    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4419    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4420    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4421
4422    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4423    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4424    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4425
4426    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4427    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4428    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4429    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4430    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4431    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4432    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4433
4434    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4435    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4436    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.")
4437    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.")
4438    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4439
4440    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4441    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4442    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4443
4444    cmdArgs = parser.parse_args()
4445    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs)
4448def Main(**kwargs):
4449    """
4450    Main function for work with TKSBrokerAPI in the console.
4451
4452    See examples:
4453    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4454    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4455    """
4456    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4457
4458    if args.debug_level:
4459        uLogger.level = 10  # always debug level by default
4460        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4461
4462    exitCode = 0
4463    start = datetime.now(tzutc())
4464    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4465        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4466        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4467    ))
4468
4469    # trying to calculate full current version:
4470    buildVersion = __version__
4471    try:
4472        v = version("tksbrokerapi")
4473        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4474
4475    except Exception:
4476        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4477
4478    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4479    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4480
4481    try:
4482        if args.version:
4483            print("TKSBrokerAPI {}".format(buildVersion))
4484            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4485
4486        else:
4487            # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader`
4488            server = TinkoffBrokerServer(
4489                token=args.token,
4490                accountId=args.account_id,
4491                useCache=not args.no_cache,
4492            )
4493
4494            # --- set some options:
4495
4496            if args.ticker:
4497                if args.ticker in server.aliasesKeys:
4498                    server.ticker = server.aliases[args.ticker]  # Replace some tickers with its aliases
4499
4500                else:
4501                    server.ticker = args.ticker
4502
4503            if args.figi:
4504                server.figi = args.figi
4505
4506            if args.depth is not None:
4507                server.depth = args.depth
4508
4509            # --- do one of commands:
4510
4511            if args.list:
4512                if args.output is not None:
4513                    server.instrumentsFile = args.output
4514
4515                server.ShowInstrumentsInfo(show=True)
4516
4517            elif args.list_xlsx:
4518                server.DumpInstrumentsAsXLSX(forceUpdate=False)
4519
4520            elif args.bonds_xlsx is not None:
4521                if args.output is not None:
4522                    server.bondsXLSXFile = args.output
4523
4524                if len(args.bonds_xlsx) == 0:
4525                    server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4526
4527                else:
4528                    server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4529
4530            elif args.search:
4531                if args.output is not None:
4532                    server.searchResultsFile = args.output
4533
4534                server.SearchInstruments(pattern=args.search[0], show=True)
4535
4536            elif args.info:
4537                if not (args.ticker or args.figi):
4538                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4539                    raise Exception("Ticker or FIGI required")
4540
4541                if args.output is not None:
4542                    server.infoFile = args.output
4543
4544                if args.ticker:
4545                    server.SearchByTicker(requestPrice=True, show=True, debug=False)  # show info and current prices by ticker name
4546
4547                else:
4548                    server.SearchByFIGI(requestPrice=True, show=True, debug=False)  # show info and current prices by FIGI id
4549
4550            elif args.calendar is not None:
4551                if args.output is not None:
4552                    server.calendarFile = args.output
4553
4554                if len(args.calendar) == 0:
4555                    bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4556
4557                else:
4558                    bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4559
4560                server.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4561
4562            elif args.price:
4563                if not (args.ticker or args.figi):
4564                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4565                    raise Exception("Ticker or FIGI required")
4566
4567                server.GetCurrentPrices(show=True)
4568
4569            elif args.prices is not None:
4570                if args.output is not None:
4571                    server.pricesFile = args.output
4572
4573                server.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4574
4575            elif args.overview:
4576                if args.output is not None:
4577                    server.overviewFile = args.output
4578
4579                server.Overview(show=True, details="full")
4580
4581            elif args.overview_digest:
4582                if args.output is not None:
4583                    server.overviewDigestFile = args.output
4584
4585                server.Overview(show=True, details="digest")
4586
4587            elif args.overview_positions:
4588                if args.output is not None:
4589                    server.overviewPositionsFile = args.output
4590
4591                server.Overview(show=True, details="positions")
4592
4593            elif args.overview_orders:
4594                if args.output is not None:
4595                    server.overviewOrdersFile = args.output
4596
4597                server.Overview(show=True, details="orders")
4598
4599            elif args.overview_analytics:
4600                if args.output is not None:
4601                    server.overviewAnalyticsFile = args.output
4602
4603                server.Overview(show=True, details="analytics")
4604
4605            elif args.deals is not None:
4606                if args.output is not None:
4607                    server.reportFile = args.output
4608
4609                if 0 <= len(args.deals) < 3:
4610                    server.Deals(
4611                        start=args.deals[0] if len(args.deals) >= 1 else None,
4612                        end=args.deals[1] if len(args.deals) == 2 else None,
4613                        show=True,  # Always show deals report in console
4614                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4615                    )
4616
4617                else:
4618                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4619                    raise Exception("Incorrect value")
4620
4621            elif args.history is not None:
4622                if args.output is not None:
4623                    server.historyFile = args.output
4624
4625                if 0 <= len(args.history) < 3:
4626                    dataReceived = server.History(
4627                        start=args.history[0] if len(args.history) >= 1 else None,
4628                        end=args.history[1] if len(args.history) == 2 else None,
4629                        interval="hour" if args.interval is None or not args.interval else args.interval,
4630                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4631                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4632                        show=True,  # shows all downloaded candles in console
4633                    )
4634
4635                    if args.render_chart is not None and dataReceived is not None:
4636                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4637
4638                        server.ShowHistoryChart(
4639                            candles=dataReceived,
4640                            interact=iChart,
4641                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4642                        )
4643
4644                else:
4645                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4646                    raise Exception("Incorrect value")
4647
4648            elif args.load_history is not None:
4649                histData = server.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4650
4651                if args.render_chart is not None and histData is not None:
4652                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4653                    server.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4654
4655                    server.ShowHistoryChart(
4656                        candles=histData,
4657                        interact=iChart,
4658                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4659                    )
4660
4661            elif args.trade is not None:
4662                if 1 <= len(args.trade) <= 5:
4663                    server.Trade(
4664                        operation=args.trade[0],
4665                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4666                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4667                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4668                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4669                    )
4670
4671                else:
4672                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4673
4674            elif args.buy is not None:
4675                if 0 <= len(args.buy) <= 4:
4676                    server.Buy(
4677                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4678                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4679                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4680                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4681                    )
4682
4683                else:
4684                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4685
4686            elif args.sell is not None:
4687                if 0 <= len(args.sell) <= 4:
4688                    server.Sell(
4689                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4690                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4691                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4692                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4693                    )
4694
4695                else:
4696                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4697
4698            elif args.order:
4699                if 4 <= len(args.order) <= 7:
4700                    server.Order(
4701                        operation=args.order[0],
4702                        orderType=args.order[1],
4703                        lots=int(args.order[2]),
4704                        targetPrice=float(args.order[3]),
4705                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4706                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4707                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4708                    )
4709
4710                else:
4711                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4712
4713            elif args.buy_limit:
4714                server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4715
4716            elif args.sell_limit:
4717                server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4718
4719            elif args.buy_stop:
4720                if 2 <= len(args.buy_stop) <= 7:
4721                    server.BuyStop(
4722                        lots=int(args.buy_stop[0]),
4723                        targetPrice=float(args.buy_stop[1]),
4724                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4725                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4726                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4727                    )
4728
4729                else:
4730                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4731
4732            elif args.sell_stop:
4733                if 2 <= len(args.sell_stop) <= 7:
4734                    server.SellStop(
4735                        lots=int(args.sell_stop[0]),
4736                        targetPrice=float(args.sell_stop[1]),
4737                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4738                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4739                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4740                    )
4741
4742                else:
4743                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4744
4745            # elif args.buy_order_grid is not None:
4746            #     # update order grid work with api v2
4747            #     if len(args.buy_order_grid) == 2:
4748            #         orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4749            #
4750            #         for order in orderParams:
4751            #             server.Order(operation="Buy", lots=order["lot"], price=order["price"])
4752            #
4753            #     else:
4754            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4755            #
4756            # elif args.sell_order_grid is not None:
4757            #     # update order grid work with api v2
4758            #     if len(args.sell_order_grid) >= 2:
4759            #         orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4760            #
4761            #         for order in orderParams:
4762            #             server.Order(operation="Sell", lots=order["lot"], price=order["price"])
4763            #
4764            #     else:
4765            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4766
4767            elif args.close_order is not None:
4768                server.CloseOrders(args.close_order)  # close only one order
4769
4770            elif args.close_orders is not None:
4771                server.CloseOrders(args.close_orders)  # close list of orders
4772
4773            elif args.close_trade:
4774                if not args.ticker:
4775                    uLogger.error("`--ticker` key is required for this operation!")
4776                    raise Exception("Ticker required")
4777
4778                server.CloseTrades([args.ticker])  # close only one trade
4779
4780            elif args.close_trades is not None:
4781                server.CloseTrades(args.close_trades)  # close trades for list of tickers
4782
4783            elif args.close_all is not None:
4784                server.CloseAll(*args.close_all)
4785
4786            elif args.limits:
4787                if args.output is not None:
4788                    server.withdrawalLimitsFile = args.output
4789
4790                server.OverviewLimits(show=True)
4791
4792            elif args.user_info:
4793                if args.output is not None:
4794                    server.userInfoFile = args.output
4795
4796                server.OverviewUserInfo(show=True)
4797
4798            elif args.account:
4799                if args.output is not None:
4800                    server.userAccountsFile = args.output
4801
4802                server.OverviewAccounts(show=True)
4803
4804            else:
4805                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4806                raise Exception("There is no command to execute")
4807
4808    except Exception:
4809        trace = tb.format_exc()
4810        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4811            if e in trace:
4812                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4813                break
4814
4815        uLogger.debug(trace)
4816        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4817        exitCode = 255  # an error occurred, must be open a ticket for this issue
4818
4819    finally:
4820        finish = datetime.now(tzutc())
4821
4822        if exitCode == 0:
4823            uLogger.debug("All operations were finished success (summary code is 0).")
4824
4825        else:
4826            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4827                os.path.abspath(uLog.defaultLogFile), exitCode,
4828            ))
4829
4830        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4831        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4832            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4833            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4834        ))
4835
4836        if not kwargs:
4837            sys.exit(exitCode)
4838
4839        else:
4840            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: